Explorar el Código

feat(marketplace): adopt agents-only v2 model

Alvin Unreal hace 4 días
padre
commit
41d538dc68
Se han modificado 44 ficheros con 2471 adiciones y 2848 borrados
  1. 11 15
      README.md
  2. 14 20
      docs/adr/002-v3-agent-resolution-foundation.md
  3. 1 2
      docs/configuration.md
  4. 11 15
      docs/marketplace.md
  5. 1 1
      docs/project-local-customization.md
  6. 1 1
      docs/tools.md
  7. 0 17
      oh-my-opencode-slim.schema.json
  8. 1 1
      package.json
  9. 466 172
      src/agents/index.ts
  10. 0 2
      src/agents/role-definitions.ts
  11. 0 1
      src/cli/config-io.ts
  12. 0 2
      src/cli/index.ts
  13. 1 24
      src/cli/marketplace.test.ts
  14. 5 58
      src/cli/marketplace.ts
  15. 1 5
      src/config/loader.test.ts
  16. 2 9
      src/config/loader.ts
  17. 4 9
      src/config/schema.test.ts
  18. 0 43
      src/config/schema.ts
  19. 28 45
      src/marketplace-contract/index.ts
  20. 2 97
      src/marketplace/activation-config.ts
  21. 64 315
      src/marketplace/activation.test.ts
  22. 118 276
      src/marketplace/activation.ts
  23. 679 0
      src/marketplace/agents-only.test.ts
  24. 12 19
      src/marketplace/compatibility.test.ts
  25. 1 12
      src/marketplace/compatibility.ts
  26. 9 14
      src/marketplace/config-references.test.ts
  27. 2 9
      src/marketplace/config-references.ts
  28. 390 396
      src/marketplace/lease.test.ts
  29. 234 492
      src/marketplace/lease.ts
  30. 0 2
      src/marketplace/paths.ts
  31. 10 14
      src/marketplace/r1-dispatch.test.ts
  32. 49 47
      src/marketplace/registry-client.test.ts
  33. 10 18
      src/marketplace/registry-client.ts
  34. 27 105
      src/marketplace/schemas.test.ts
  35. 113 146
      src/marketplace/schemas.ts
  36. 0 1
      src/marketplace/service.ts
  37. 15 30
      src/marketplace/status.test.ts
  38. 3 30
      src/marketplace/status.ts
  39. 64 73
      src/marketplace/store.test.ts
  40. 0 1
      src/marketplace/store.ts
  41. 119 219
      src/tools/marketplace.test.ts
  42. 2 69
      src/tools/marketplace.ts
  43. 0 18
      src/tools/preset-switch.test.ts
  44. 1 3
      src/tools/preset-switch.ts

+ 11 - 15
README.md

@@ -49,8 +49,8 @@ The main idea is simple: instead of forcing one model to do everything, the plug
 - **[Code intelligence tools](docs/tools.md)** - LSP tools, AST-aware search
 - **[Code intelligence tools](docs/tools.md)** - LSP tools, AST-aware search
   across 25 languages, and built-in MCPs for docs and GitHub code
   across 25 languages, and built-in MCPs for docs and GitHub code
 - **[Marketplace](docs/marketplace.md)** - install registry packages or import
 - **[Marketplace](docs/marketplace.md)** - install registry packages or import
-  local agent/profile packages explicitly; startup and local reads stay offline
-  and changes apply after reload
+  local agent packages explicitly; startup and local reads stay offline and
+  changes apply after reload
   search.
   search.
 - **[Fully customizable](docs/configuration.md)** - custom agents, prompt
 - **[Fully customizable](docs/configuration.md)** - custom agents, prompt
   overrides, per-agent skill/MCP permissions, and
   overrides, per-agent skill/MCP permissions, and
@@ -117,12 +117,12 @@ Startup and local marketplace reads are offline. Explicit registry install and
 update operations use bounded HTTPS requests to the fixed beta registry.
 update operations use bounded HTTPS requests to the fixed beta registry.
 Package manifests are data-only, exact-version locked, and stored under the XDG data directory.
 Package manifests are data-only, exact-version locked, and stored under the XDG data directory.
 Installation and preset activation are separate: install a package, then enable
 Installation and preset activation are separate: install a package, then enable
-it in the active preset. Activated agents and profiles apply after the next
+it in the active preset. Activated agents apply after the next
 OpenCode session/reload; the live registry is never hot-swapped.
 OpenCode session/reload; the live registry is never hot-swapped.
-Manifests include bounded author, tag, license, plugin/role-contract,
-routing/delegation, role-target, capability, and override metadata. `instructions`
-is the single package instruction surface; profile packages declare explicit
-append/replace composition semantics. Executable fields,
+Manifests include bounded author, tag, license, plugin compatibility, routing,
+model policy, exact skills/MCPs/tools, and prompt metadata. Agents may extend
+one built-in specialist with explicit append/replace composition semantics.
+Executable fields,
 package-to-package dependencies, and arbitrary file maps are rejected.
 package-to-package dependencies, and arbitrary file maps are rejected.
 
 
 ```bash
 ```bash
@@ -134,7 +134,6 @@ bunx oh-my-opencode-slim marketplace verify [author/name]
 bunx oh-my-opencode-slim marketplace import ./package-v2.json --update
 bunx oh-my-opencode-slim marketplace import ./package-v2.json --update
 bunx oh-my-opencode-slim marketplace update community/example
 bunx oh-my-opencode-slim marketplace update community/example
 bunx oh-my-opencode-slim marketplace enable author/name
 bunx oh-my-opencode-slim marketplace enable author/name
-bunx oh-my-opencode-slim marketplace profile librarian author/profile
 bunx oh-my-opencode-slim marketplace disable author/name
 bunx oh-my-opencode-slim marketplace disable author/name
 bunx oh-my-opencode-slim marketplace remove author/name
 bunx oh-my-opencode-slim marketplace remove author/name
 bunx oh-my-opencode-slim marketplace status
 bunx oh-my-opencode-slim marketplace status
@@ -144,13 +143,10 @@ Use `import` explicitly for local author files; `import --update` requires an
 existing package and a strictly newer version. Unversioned registry install
 existing package and a strictly newer version. Unversioned registry install
 selects the highest compatible version, while `ID@version` selects exactly
 selects the highest compatible version, while `ID@version` selects exactly
 that version. Registry update requires an installed package and is strictly
 that version. Registry update requires an installed package and is strictly
-monotonic. `enable` activates
-an installed `agent` package in the active preset as a separately named
-role-derived agent. `profile` selects at most one installed `profile` package
-per supported specialist role; `--clear` writes a tombstone. Required skills
-and MCPs are preflighted against built-in capabilities and on-disk host
-configuration; missing required dependencies disable that package for the
-session. Optional requirements stay unavailable and are never auto-installed.
+monotonic. `enable` activates an installed agent package in the active preset as
+a separately named agent. Declared skills and MCPs are preflighted against
+built-in capabilities and on-disk host configuration; missing dependencies
+disable that agent for the session.
 Startup reads only the local store and never contacts a registry. The
 Startup reads only the local store and never contacts a registry. The
 orchestrator can perform the same lifecycle
 orchestrator can perform the same lifecycle
 with the in-session `marketplace` tool; see
 with the in-session `marketplace` tool; see

+ 14 - 20
docs/adr/002-v3-agent-resolution-foundation.md

@@ -14,9 +14,9 @@ conversation.
 
 
 The supported specialist role catalog is explicit: `explorer`, `librarian`,
 The supported specialist role catalog is explicit: `explorer`, `librarian`,
 `oracle`, `designer`, `fixer`, and `observer`. Their reusable
 `oracle`, `designer`, `fixer`, and `observer`. Their reusable
-`RoleDefinition`s are the only role baselines available for future derived
-agents and profiles. `orchestrator`, `council`, and `councillor` remain special
-agents and are not profile targets.
+`RoleDefinition`s are the only built-in baselines available to marketplace
+agents. `orchestrator`, `council`, and `councillor` remain special agents and
+cannot be marketplace extension targets.
 
 
 Presets are structured objects:
 Presets are structured objects:
 
 
@@ -27,22 +27,19 @@ Presets are structured objects:
   },
   },
   "marketplace": {
   "marketplace": {
     "agents": ["community/example"],
     "agents": ["community/example"],
-    "profiles": { "oracle": "community/oracle-profile" }
   }
   }
 }
 }
 ```
 ```
 
 
 Activation arrays are optional so omitted and explicit empty values remain
 Activation arrays are optional so omitted and explicit empty values remain
-distinguishable during typed merges. Package IDs are trimmed and unique; profile
-targets are limited to supported specialist roles and `null` is a removal
-tombstone. Phase 3 resolves installed packages from the local store for the
-effective active preset, preflights required skills/MCPs, and applies derived
-agents and specialist profiles into the one `ResolvedAgentRegistry`. Package
-state remains outside `PluginConfigSchema`.
+distinguishable during typed merges. Package IDs are trimmed and unique. The
+runtime resolves installed agents from the local store for the effective active
+preset, preflights declared skills/MCPs, and applies them into the one
+`ResolvedAgentRegistry`. Package state remains outside `PluginConfigSchema`.
 
 
-Prompt resolution is a pure ordered pipeline: built-in baseline, package
-append/replace (reserved for later phases), preset override, root override,
-host-supported fields, then user append files. Mandatory plugin suffixes are
+Prompt resolution is a pure ordered pipeline: optional built-in baseline,
+package append/replace, owner replacement, owner append, then host-supported
+fields. Mandatory plugin suffixes are
 added last, so replacements cannot remove workflow or role-policy gates.
 added last, so replacements cannot remove workflow or role-policy gates.
 Inline replacement wins over a replacement file, and append files remain
 Inline replacement wins over a replacement file, and append files remain
 supported. Inputs are construction-time deterministic and do not contain
 supported. Inputs are construction-time deterministic and do not contain
@@ -57,10 +54,8 @@ configuration remains editable and higher precedence than package content.
 Task controls are editable defaults; only documented plugin gates are applied
 Task controls are editable defaults; only documented plugin gates are applied
 after host projection.
 after host projection.
 
 
-Required dependencies for future packages will be preflighted from built-in
-capabilities and on-disk host configuration. Missing required dependencies
-disable a package for that session; optional dependencies remain unavailable.
-Those package behaviors belong to later phases, not this ADR's implementation.
+Declared skills and MCPs are preflighted from built-in capabilities and on-disk
+host configuration. Missing dependencies disable an agent for that session.
 
 
 ## Resolution truth table
 ## Resolution truth table
 
 
@@ -78,7 +73,6 @@ Those package behaviors belong to later phases, not this ADR's implementation.
 | Permissions | defaults | user rules | editable rules first, immutable gates after host projection | Implemented |
 | Permissions | defaults | user rules | editable rules first, immutable gates after host projection | Implemented |
 | Routing | resolved enabled agents | none | canonical deterministic name ordering | Implemented |
 | Routing | resolved enabled agents | none | canonical deterministic name ordering | Implemented |
 | Preset | user preset | project preset | structural merge by name; active agent layers merge once in registry | Implemented |
 | Preset | user preset | project preset | structural merge by name; active agent layers merge once in registry | Implemented |
-| Marketplace agent | role baseline | package config | append-only role derivation | Implemented |
-| Marketplace profile | role baseline | one selected profile | one profile per explicit supported role; null is tombstone | Implemented |
-| Dependency preflight | package requirements | capabilities | required missing dependency disables package | Implemented |
+| Marketplace agent | optional builtin baseline | package config | standalone or one-builtin extension | Implemented |
+| Dependency preflight | declared skills/MCPs | host availability | missing dependency disables agent | Implemented |
 | Package state | plugin config | lock/store | separate parsed state, exact versions, offline startup | Implemented |
 | Package state | plugin config | lock/store | separate parsed state, exact versions, offline startup | Implemented |

+ 1 - 2
docs/configuration.md

@@ -495,8 +495,7 @@ for the CLI, in-session `marketplace` tool, and status diagnostics.
   "presets": {
   "presets": {
     "work": {
     "work": {
       "marketplace": {
       "marketplace": {
-        "agents": ["community/example"],
-        "profiles": { "oracle": "community/oracle-profile" }
+        "agents": ["community/oracle-agent"]
       }
       }
     }
     }
   }
   }

+ 11 - 15
docs/marketplace.md

@@ -6,8 +6,8 @@ make bounded HTTPS requests to the beta registry.
 
 
 Package manifests are data-only, exact-version locked, and stored under the
 Package manifests are data-only, exact-version locked, and stored under the
 XDG data directory. Installation and preset activation are separate: install
 XDG data directory. Installation and preset activation are separate: install
-a package, then enable it in the active preset. Activated agents and
-profiles apply after the next OpenCode session or reload. The live agent
+a package, then enable it in the active preset. Activated agents apply after
+the next OpenCode session or reload. The live agent
 registry is never hot-swapped.
 registry is never hot-swapped.
 
 
 ## CLI
 ## CLI
@@ -22,8 +22,6 @@ bunx oh-my-opencode-slim marketplace list
 bunx oh-my-opencode-slim marketplace show author/name
 bunx oh-my-opencode-slim marketplace show author/name
 bunx oh-my-opencode-slim marketplace verify [author/name]
 bunx oh-my-opencode-slim marketplace verify [author/name]
 bunx oh-my-opencode-slim marketplace enable author/name
 bunx oh-my-opencode-slim marketplace enable author/name
-bunx oh-my-opencode-slim marketplace profile librarian author/profile
-bunx oh-my-opencode-slim marketplace profile oracle --clear
 bunx oh-my-opencode-slim marketplace disable author/name
 bunx oh-my-opencode-slim marketplace disable author/name
 bunx oh-my-opencode-slim marketplace remove author/name
 bunx oh-my-opencode-slim marketplace remove author/name
 bunx oh-my-opencode-slim marketplace status [--json]
 bunx oh-my-opencode-slim marketplace status [--json]
@@ -34,10 +32,9 @@ absolute local source path in the lockfile. Add `--update` to import a strictly
 newer version into an existing package. Registry `install` accepts an ID or an
 newer version into an existing package. Registry `install` accepts an ID or an
 exact `ID@version`; an unversioned ID selects the highest compatible version.
 exact `ID@version`; an unversioned ID selects the highest compatible version.
 Registry `update` requires an installed package and selects only a strictly
 Registry `update` requires an installed package and selects only a strictly
-newer compatible version. `enable` activates an installed `agent` package in the active
-preset as a separately named role-derived agent. `profile` selects at most
-one installed `profile` package per supported specialist role; `--clear`
-writes a tombstone.
+newer compatible version. `enable` activates an installed agent package in the
+active preset as a separately named agent. An agent may optionally extend one
+built-in specialist.
 
 
 `status` reports installed packages, configured activation, live-session
 `status` reports installed packages, configured activation, live-session
 agents when used from the in-session tool, diagnostics, and whether a
 agents when used from the in-session tool, diagnostics, and whether a
@@ -70,19 +67,18 @@ read (for example EACCES).
 | Field | Meaning |
 | Field | Meaning |
 |-------|---------|
 |-------|---------|
 | installed | Exact locked versions in the local store |
 | installed | Exact locked versions in the local store |
-| configured_agents / configured_profiles | Active-preset activation on disk |
+| configured_agents | Active-preset activation on disk |
 | live_packages | Packages already in this session's registry, with version, digest, and runtime name |
 | live_packages | Packages already in this session's registry, with version, digest, and runtime name |
 | diagnostics | Store and activation issues (missing, corrupt, operational, collision, missing required dependency, invalid alias, retired), labeled `disk` or `live` |
 | diagnostics | Store and activation issues (missing, corrupt, operational, collision, missing required dependency, invalid alias, retired), labeled `disk` or `live` |
 | reload_required | `true`/`false` when live and desired identities can be compared; `unknown` for CLI and when store/desired resolution failed operationally |
 | reload_required | `true`/`false` when live and desired identities can be compared; `unknown` for CLI and when store/desired resolution failed operationally |
 
 
 ## Limits
 ## Limits
 
 
-- The beta registry is fixed at `https://registry.ohmyopencodeslim.com/v1/`;
+- The beta registry is fixed at `https://registry.ohmyopencodeslim.com/v2/`;
   configurable registries and redirects are not supported.
   configurable registries and redirects are not supported.
-- Required skills and MCPs are preflighted against built-in capabilities
-  and on-disk host configuration. Missing required dependencies disable
-  that package for the session. Optional requirements stay unavailable
-  and are never auto-installed.
+- Declared skills and MCPs are preflighted against built-in capabilities and
+  on-disk host configuration. Missing dependencies disable that package for
+  the session; nothing is auto-installed.
 - Startup and local `list`, `show`, `verify`, `status`, activation, and removal
 - Startup and local `list`, `show`, `verify`, `status`, activation, and removal
   read only the local store and never contact a registry.
   read only the local store and never contact a registry.
 
 
@@ -90,7 +86,7 @@ read (for example EACCES).
 
 
 Registry CI and static site tooling can import the narrow
 Registry CI and static site tooling can import the narrow
 `oh-my-opencode-slim/marketplace-contract` package subpath. It provides the
 `oh-my-opencode-slim/marketplace-contract` package subpath. It provides the
-schema-v1 and schema-v2 indexes (including retirement tombstones), deterministic
+schema-v3 indexes (including retirement tombstones), deterministic
 artifact paths, manifest-summary projection, selector resolution, and the same
 artifact paths, manifest-summary projection, selector resolution, and the same
 canonical bundle SHA-256 digest used by the plugin store. The public contract
 canonical bundle SHA-256 digest used by the plugin store. The public contract
 does not add root-package exports.
 does not add root-package exports.

+ 1 - 1
docs/project-local-customization.md

@@ -14,7 +14,7 @@ This document describes how to configure and customize oh-my-opencode-slim on a
 
 
 | Feature | Scope / Location | Description |
 | Feature | Scope / Location | Description |
 |---|---|---|
 |---|---|---|
-| **Configuration file** | `.opencode/oh-my-opencode-slim.json[c]` | Project-level configuration file that overrides global user settings, merging presets, agent profiles, and multiplexer integration. |
+| **Configuration file** | `.opencode/oh-my-opencode-slim.json[c]` | Project-level configuration file that overrides global user settings, merging presets, agent configuration, and multiplexer integration. |
 | **Custom agents** | `agents` configuration block | Define new specialized agents by keying them under `agents.<custom-name>` with required `model`, custom system `prompt`, and optional routing guidance. |
 | **Custom agents** | `agents` configuration block | Define new specialized agents by keying them under `agents.<custom-name>` with required `model`, custom system `prompt`, and optional routing guidance. |
 | **Built-in prompt overrides** | `.opencode/oh-my-opencode-slim/<agent>.md` | Override the built-in system prompt for any agent (e.g. `oracle.md`, `explorer.md`, `orchestrator.md`, or custom agents). Acts as the default when no inline `prompt` is set in config. |
 | **Built-in prompt overrides** | `.opencode/oh-my-opencode-slim/<agent>.md` | Override the built-in system prompt for any agent (e.g. `oracle.md`, `explorer.md`, `orchestrator.md`, or custom agents). Acts as the default when no inline `prompt` is set in config. |
 | **Append prompts** | `.opencode/oh-my-opencode-slim/<agent>_append.md` | Append additional rules or guidelines to the existing base (inline, file, or default built-in) prompt without overriding it completely. |
 | **Append prompts** | `.opencode/oh-my-opencode-slim/<agent>_append.md` | Append additional rules or guidelines to the existing base (inline, file, or default built-in) prompt without overriding it completely. |

+ 1 - 1
docs/tools.md

@@ -93,7 +93,7 @@ lifecycle, cancellation, and explicit-wait edge cases behind these tools.
 | `marketplace` | Install, inspect, and activate local offline marketplace packages |
 | `marketplace` | Install, inspect, and activate local offline marketplace packages |
 
 
 `marketplace` is orchestrator-only. list/show/verify/status are read-only.
 `marketplace` is orchestrator-only. list/show/verify/status are read-only.
-install/import/update/enable/disable/profile/remove write store/config only
+install/import/update/enable/disable/remove write store/config only
 and report reload only when disk activation differs from the live session.
 and report reload only when disk activation differs from the live session.
 The CLI reports reload status as unknown. In-session status is also
 The CLI reports reload status as unknown. In-session status is also
 unknown when the store or desired activation cannot be read. There is no
 unknown when the store or desired activation cannot be read. There is no

+ 0 - 17
oh-my-opencode-slim.schema.json

@@ -515,23 +515,6 @@
                   "type": "string",
                   "type": "string",
                   "minLength": 1
                   "minLength": 1
                 }
                 }
-              },
-              "profiles": {
-                "type": "object",
-                "propertyNames": {
-                  "type": "string"
-                },
-                "additionalProperties": {
-                  "anyOf": [
-                    {
-                      "type": "string",
-                      "minLength": 1
-                    },
-                    {
-                      "type": "null"
-                    }
-                  ]
-                }
               }
               }
             },
             },
             "additionalProperties": false
             "additionalProperties": false

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "oh-my-opencode-slim",
   "name": "oh-my-opencode-slim",
-  "version": "3.0.0-beta.2",
+  "version": "3.0.0-beta.3",
   "packageManager": "bun@1.3.14",
   "packageManager": "bun@1.3.14",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "main": "dist/index.js",
   "main": "dist/index.js",

+ 466 - 172
src/agents/index.ts

@@ -17,14 +17,13 @@ import { type HostConfigSnapshot, RuntimeConfig } from '../config/runtime';
 import { applyOrchestratorModelConfig } from '../config/strip-orchestrator-model';
 import { applyOrchestratorModelConfig } from '../config/strip-orchestrator-model';
 import {
 import {
   type ActivatedMarketplaceAgent,
   type ActivatedMarketplaceAgent,
-  type ActivatedMarketplaceProfile,
-  boundedPackageOverride,
   composePackagePrompt,
   composePackagePrompt,
   type MarketplaceActivationPlan,
   type MarketplaceActivationPlan,
   type MarketplaceDiagnostic,
   type MarketplaceDiagnostic,
   reservedRuntimeNames,
   reservedRuntimeNames,
   resolveMarketplaceActivation,
   resolveMarketplaceActivation,
 } from '../marketplace/activation';
 } from '../marketplace/activation';
+import { MARKETPLACE_TOOL_NAMES } from '../marketplace/schemas';
 import type {
 import type {
   MarketplaceLivePackage,
   MarketplaceLivePackage,
   MarketplaceLiveSnapshot,
   MarketplaceLiveSnapshot,
@@ -49,7 +48,6 @@ import {
 import {
 import {
   ROLE_DEFINITIONS,
   ROLE_DEFINITIONS,
   renderRoleRoutingBlock,
   renderRoleRoutingBlock,
-  type SpecialistRole,
   SUPPORTED_SPECIALIST_ROLES,
   SUPPORTED_SPECIALIST_ROLES,
 } from './role-definitions';
 } from './role-definitions';
 import {
 import {
@@ -258,22 +256,11 @@ function buildMarketplaceLive(
   marketplace: MarketplaceActivationPlan,
   marketplace: MarketplaceActivationPlan,
   runtimeNameByPackageId: Readonly<Record<string, string>>,
   runtimeNameByPackageId: Readonly<Record<string, string>>,
 ): MarketplaceLivePackage[] {
 ): MarketplaceLivePackage[] {
-  const registered = new Map<
-    string,
-    { version: string; digest: string; kind: 'agent' | 'profile' }
-  >();
+  const registered = new Map<string, { version: string; digest: string }>();
   for (const activated of marketplace.agents) {
   for (const activated of marketplace.agents) {
     registered.set(activated.packageId, {
     registered.set(activated.packageId, {
       version: activated.version,
       version: activated.version,
       digest: activated.digest,
       digest: activated.digest,
-      kind: 'agent',
-    });
-  }
-  for (const activated of marketplace.profiles.values()) {
-    registered.set(activated.packageId, {
-      version: activated.version,
-      digest: activated.digest,
-      kind: 'profile',
     });
     });
   }
   }
   const live: MarketplaceLivePackage[] = [];
   const live: MarketplaceLivePackage[] = [];
@@ -287,7 +274,6 @@ function buildMarketplaceLive(
       version: meta.version,
       version: meta.version,
       digest: meta.digest,
       digest: meta.digest,
       runtimeName,
       runtimeName,
-      kind: meta.kind,
     });
     });
   }
   }
   return live;
   return live;
@@ -363,6 +349,21 @@ function getPrimaryModelFromOverride(
   return resolvePrimaryModelValue(override?.model);
   return resolvePrimaryModelValue(override?.model);
 }
 }
 
 
+function hasExplicitVariantOverride(
+  override: AgentOverrideConfig | undefined,
+): boolean {
+  if (override?.variant !== undefined) return true;
+  if (!Array.isArray(override?.model) || override.model.length === 0) {
+    return false;
+  }
+  const primary = override.model[0];
+  return typeof primary !== 'string' && primary.variant !== undefined;
+}
+
+function stringVariant(value: unknown): string | undefined {
+  return typeof value === 'string' ? value : undefined;
+}
+
 const isInternalOnly = (name: string): boolean =>
 const isInternalOnly = (name: string): boolean =>
   name === 'councillor' || name.startsWith('councillor-');
   name === 'councillor' || name.startsWith('councillor-');
 
 
@@ -443,8 +444,12 @@ function applyOverrides(
   agent: AgentDefinition,
   agent: AgentDefinition,
   override: AgentOverrideConfig,
   override: AgentOverrideConfig,
 ): void {
 ): void {
-  if (override.model) {
+  if (override.model !== undefined) {
     if (Array.isArray(override.model)) {
     if (Array.isArray(override.model)) {
+      // A model override replaces a marketplace candidate chain, including
+      // its package-level primary variant. Re-apply only the owner-provided
+      // variant below.
+      delete agent.config.variant;
       agent._modelArray = override.model.map((m) =>
       agent._modelArray = override.model.map((m) =>
         typeof m === 'string' ? { id: m } : m,
         typeof m === 'string' ? { id: m } : m,
       );
       );
@@ -478,10 +483,16 @@ function applyOverrides(
         agent.config.variant = primaryModel.variant;
         agent.config.variant = primaryModel.variant;
       }
       }
     } else {
     } else {
+      // Marketplace agents may have installed an explicit package fallback
+      // chain before owner overrides are applied. A scalar owner model is a
+      // replacement, not an additional candidate, so discard that chain and
+      // its package-level primary variant.
+      delete agent._modelArray;
+      delete agent.config.variant;
       agent.config.model = override.model;
       agent.config.model = override.model;
     }
     }
   }
   }
-  if (override.variant) agent.config.variant = override.variant;
+  if (override.variant !== undefined) agent.config.variant = override.variant;
   if (override.temperature !== undefined)
   if (override.temperature !== undefined)
     agent.config.temperature = override.temperature;
     agent.config.temperature = override.temperature;
   if (override.color) agent.config.color = override.color;
   if (override.color) agent.config.color = override.color;
@@ -514,12 +525,25 @@ function applyModelInheritance(
 ): void {
 ): void {
   if (override?.model !== undefined) return;
   if (override?.model !== undefined) return;
 
 
-  if (
-    override?.inheritModelFrom === 'session' ||
-    (override?.inheritModelFrom === 'orchestrator' &&
-      orchestratorModel === undefined)
-  ) {
+  if (override?.inheritModelFrom === 'session') {
+    delete agent._modelArray;
     delete agent.config.model;
     delete agent.config.model;
+    if (!hasExplicitVariantOverride(override)) {
+      delete agent.config.variant;
+    }
+    return;
+  }
+
+  if (override?.inheritModelFrom === 'orchestrator') {
+    delete agent._modelArray;
+    if (orchestratorModel === undefined) {
+      delete agent.config.model;
+    } else {
+      agent.config.model = orchestratorModel;
+    }
+    if (!hasExplicitVariantOverride(override)) {
+      delete agent.config.variant;
+    }
   }
   }
 }
 }
 
 
@@ -617,6 +641,11 @@ export function applyModelInheritanceToConfig(
     }
     }
 
 
     const agentConfig = entry as Record<string, unknown>;
     const agentConfig = entry as Record<string, unknown>;
+    const hostAgent =
+      runtime.hostAgent(agentName) ?? runtime.hostAgent(resolvedName);
+    const hasHostVariant = hostAgent?.variant !== undefined;
+    const preserveVariant =
+      hasExplicitVariantOverride(override) || hasHostVariant;
     if (override.inheritModelFrom === 'session') {
     if (override.inheritModelFrom === 'session') {
       delete agentConfig.model;
       delete agentConfig.model;
     } else if (orchestratorModel === undefined) {
     } else if (orchestratorModel === undefined) {
@@ -624,6 +653,9 @@ export function applyModelInheritanceToConfig(
     } else {
     } else {
       agentConfig.model = orchestratorModel;
       agentConfig.model = orchestratorModel;
     }
     }
+    if (!preserveVariant) {
+      delete agentConfig.variant;
+    }
   }
   }
 }
 }
 
 
@@ -820,61 +852,244 @@ function marketplaceSkillList(
   return uniqueNames([...roleSkills, ...extraSkills]);
   return uniqueNames([...roleSkills, ...extraSkills]);
 }
 }
 
 
-function applyMarketplaceCapabilities(
+const READONLY_MARKETPLACE_TOOLS = [
+  'read',
+  'glob',
+  'grep',
+  'ast_grep_search',
+  'webfetch',
+  'websearch',
+] as const;
+
+const NON_MARKETPLACE_TOOL_NAMES = [
+  'task',
+  'acp_run',
+  'lsp',
+  'list',
+  'codesearch',
+] as const;
+
+function restrictMarketplaceNames(
+  ceiling: readonly string[],
+  ownerValues: readonly string[] | undefined,
+): string[] {
+  if (ownerValues === undefined) return [...ceiling];
+  const denied = new Set(
+    ownerValues
+      .filter((value) => value.startsWith('!'))
+      .map((value) => value.slice(1)),
+  );
+  const explicit = new Set(
+    ownerValues.filter((value) => value !== '*' && !value.startsWith('!')),
+  );
+  const allowsAll = ownerValues.includes('*');
+  return ceiling.filter(
+    (name) => !denied.has(name) && (allowsAll || explicit.has(name)),
+  );
+}
+
+function marketplaceCapabilityCeilings(
   agent: AgentDefinition,
   agent: AgentDefinition,
-  activated: ActivatedMarketplaceAgent | ActivatedMarketplaceProfile,
-): void {
-  const permission = normalizePermission(agent.config.permission);
-  if (hasWildcardDeny(permission)) {
-    agent.config.permission = permission as SDKAgentConfig['permission'];
-    return;
-  }
+  activated: ActivatedMarketplaceAgent,
+  override: AgentOverrideConfig | undefined,
+): {
+  tools: readonly string[];
+  skills: readonly string[];
+  mcps: readonly string[];
+} {
   const role = agent.baseRole ? ROLE_DEFINITIONS[agent.baseRole] : undefined;
   const role = agent.baseRole ? ROLE_DEFINITIONS[agent.baseRole] : undefined;
-  for (const tool of activated.manifest.capabilities.tools) {
-    permission[tool] = 'allow';
-  }
-  for (const capability of activated.manifest.capabilities.permissions) {
-    if (capability === 'filesystem.read') permission.read = 'allow';
+  const baselineTools = role
+    ? role.permissionPolicy === 'read-only'
+      ? [...READONLY_MARKETPLACE_TOOLS]
+      : [...MARKETPLACE_TOOL_NAMES]
+    : [];
+  const tools = role
+    ? uniqueNames([...baselineTools, ...activated.manifest.tools])
+    : [...activated.manifest.tools];
+  const skills = restrictMarketplaceNames(
+    role
+      ? uniqueNames([...role.defaultSkills, ...activated.requiredSkills])
+      : activated.requiredSkills,
+    override?.skills,
+  );
+  const mcps = restrictMarketplaceNames(
+    role
+      ? uniqueNames([...role.defaultMcps, ...activated.requiredMcps])
+      : activated.requiredMcps,
+    override?.mcps,
+  );
+  return { tools, skills, mcps };
+}
+
+function applyRestrictiveMarketplacePermission(
+  target: PermissionRecord,
+  source: unknown,
+  tools: ReadonlySet<string>,
+  skills: ReadonlySet<string>,
+  mcps: ReadonlySet<string>,
+  applyWildcard = true,
+): void {
+  const record = normalizePermission(source);
+  const wildcard = applyWildcard ? record['*'] : undefined;
+  const applyRestriction = (key: string, value: unknown): void => {
+    if (value !== 'deny' && value !== 'ask') return;
+    if (target[key] !== 'deny' || value === 'deny') {
+      target[key] = value;
+    }
+  };
+  const skillIsGloballyDenied = (): boolean => {
+    if (target.skill === 'deny') return true;
     if (
     if (
-      capability === 'filesystem.write' &&
-      role?.permissionPolicy === 'read-write'
+      target.skill &&
+      typeof target.skill === 'object' &&
+      !Array.isArray(target.skill)
     ) {
     ) {
-      permission.edit = 'allow';
-      permission.write = 'allow';
+      return (target.skill as PermissionRecord)['*'] === 'deny';
+    }
+    return false;
+  };
+  const applySkillRestriction = (
+    skillNames: ReadonlySet<string>,
+    value: unknown,
+  ): void => {
+    if (value === 'deny') {
+      target.skill = 'deny';
+      return;
+    }
+    if (value !== 'ask' || skillIsGloballyDenied()) return;
+    const targetSkills = normalizePermission(target.skill);
+    for (const skill of skillNames) {
+      if (targetSkills[skill] !== 'deny') targetSkills[skill] = 'ask';
     }
     }
-    if (capability === 'network.fetch') permission.webfetch = 'allow';
-    if (capability === 'session.delegate') permission.task = 'allow';
+    target.skill = targetSkills;
+  };
+
+  // The package projection always has a default-deny wildcard. A restrictive
+  // owner/host wildcard must therefore be projected onto every package
+  // surface explicitly; changing only `*` would leave per-tool allows in
+  // place and v2's last-match-wins evaluator would widen the package again.
+  for (const tool of tools) applyRestriction(tool, wildcard);
+  for (const mcp of mcps) applyRestriction(mcp, wildcard);
+  applySkillRestriction(skills, wildcard);
+
+  for (const tool of tools) {
+    applyRestriction(tool, record[tool]);
+  }
+  const skillPermission = record.skill;
+  applySkillRestriction(skills, skillPermission);
+  if (
+    skillPermission &&
+    typeof skillPermission === 'object' &&
+    !Array.isArray(skillPermission)
+  ) {
+    applySkillRestriction(skills, (skillPermission as PermissionRecord)['*']);
+    if (!skillIsGloballyDenied()) {
+      const effectiveTargetSkills = normalizePermission(target.skill);
+      for (const skill of skills) {
+        const value = (skillPermission as PermissionRecord)[skill];
+        if (value === 'deny') effectiveTargetSkills[skill] = 'deny';
+        else if (
+          value === 'ask' &&
+          !skillIsGloballyDenied() &&
+          effectiveTargetSkills[skill] !== 'deny'
+        )
+          effectiveTargetSkills[skill] = 'ask';
+      }
+      target.skill = effectiveTargetSkills;
+    }
+  }
+  for (const mcp of mcps) {
+    applyRestriction(mcp, record[mcp]);
   }
   }
-  agent.config.permission =
-    permission as unknown as SDKAgentConfig['permission'];
 }
 }
 
 
-function extraMarketplaceMcps(
-  plan: MarketplaceActivationPlan | undefined,
-  agentName: string,
-  baseRole: string | undefined,
-): readonly string[] {
-  if (!plan) return [];
-  const derived = plan.agents.find(
-    (entry) => entry.manifest.agentName === agentName,
+function projectMarketplacePermission(
+  packagePermission: unknown,
+  hostPermission: unknown,
+  tools: readonly string[],
+  skills: readonly string[],
+  mcps: readonly string[],
+  availableMcpNames: readonly string[],
+  hostTools: unknown,
+): PermissionRecord {
+  const result: PermissionRecord = { '*': 'deny' };
+  const allowedTools = new Set(tools);
+  for (const tool of MARKETPLACE_TOOL_NAMES) {
+    result[tool] = allowedTools.has(tool) ? 'allow' : 'deny';
+  }
+  for (const tool of NON_MARKETPLACE_TOOL_NAMES) result[tool] = 'deny';
+  result.skill = Object.fromEntries(skills.map((skill) => [skill, 'allow']));
+  const allowedMcps = new Set(mcps);
+  for (const mcp of availableMcpNames) {
+    const key = `${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`;
+    result[key] = allowedMcps.has(mcp) ? 'allow' : 'deny';
+  }
+
+  const toolSet = new Set(tools);
+  const skillSet = new Set(skills);
+  const mcpSet = new Set(
+    mcps.map((mcp) => `${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`),
+  );
+  applyRestrictiveMarketplacePermission(
+    result,
+    packagePermission,
+    toolSet,
+    skillSet,
+    mcpSet,
+    false,
+  );
+  applyRestrictiveMarketplacePermission(
+    result,
+    hostPermission,
+    toolSet,
+    skillSet,
+    mcpSet,
+  );
+
+  if (hostTools && typeof hostTools === 'object' && !Array.isArray(hostTools)) {
+    for (const [tool, value] of Object.entries(
+      hostTools as Record<string, unknown>,
+    )) {
+      if (toolSet.has(tool) && value === false) result[tool] = 'deny';
+    }
+  }
+  return result;
+}
+
+function applyMarketplaceCapabilities(
+  agent: AgentDefinition,
+  ceilings: ReturnType<typeof marketplaceCapabilityCeilings>,
+): void {
+  const permission: PermissionRecord = { '*': 'deny' };
+  const allowedTools = new Set(ceilings.tools);
+  for (const tool of MARKETPLACE_TOOL_NAMES) {
+    permission[tool] = allowedTools.has(tool) ? 'allow' : 'deny';
+  }
+  for (const tool of NON_MARKETPLACE_TOOL_NAMES) permission[tool] = 'deny';
+  permission.skill = Object.fromEntries(
+    ceilings.skills.map((skill) => [skill, 'allow']),
   );
   );
-  if (derived) return derived.requiredMcps;
-  if (!baseRole) return [];
-  return plan.profiles.get(baseRole as SpecialistRole)?.requiredMcps ?? [];
+  for (const mcp of ceilings.mcps) {
+    permission[`${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`] = 'allow';
+  }
+  applyRestrictiveMarketplacePermission(
+    permission,
+    agent.config.permission,
+    new Set(ceilings.tools),
+    new Set(ceilings.skills),
+    new Set(
+      ceilings.mcps.map((mcp) => `${mcp.replace(/[^a-zA-Z0-9_-]/g, '_')}_*`),
+    ),
+  );
+  agent.config.permission =
+    permission as unknown as SDKAgentConfig['permission'];
 }
 }
 
 
-function extraMarketplaceSkills(
+function marketplacePackage(
   plan: MarketplaceActivationPlan | undefined,
   plan: MarketplaceActivationPlan | undefined,
   agentName: string,
   agentName: string,
-  baseRole: string | undefined,
-): readonly string[] {
-  if (!plan) return [];
-  const derived = plan.agents.find(
-    (entry) => entry.manifest.agentName === agentName,
-  );
-  if (derived) return derived.requiredSkills;
-  if (!baseRole) return [];
-  return plan.profiles.get(baseRole as SpecialistRole)?.requiredSkills ?? [];
+): ActivatedMarketplaceAgent | undefined {
+  return plan?.agents.find((entry) => entry.manifest.agentName === agentName);
 }
 }
 
 
 // Agent Classification
 // Agent Classification
@@ -961,15 +1176,12 @@ function marketplaceRoutingGuidance(
   const derived = plan?.agents.find(
   const derived = plan?.agents.find(
     (entry) => entry.manifest.agentName === agent.name,
     (entry) => entry.manifest.agentName === agent.name,
   );
   );
-  const profile = agent.baseRole
-    ? plan?.profiles.get(agent.baseRole)
-    : undefined;
-  const manifest = derived?.manifest ?? profile?.manifest;
+  const manifest = derived?.manifest;
   if (!manifest) return undefined;
   if (!manifest) return undefined;
   return [
   return [
     `- Package: ${manifest.displayName}`,
     `- Package: ${manifest.displayName}`,
     `- ${manifest.routing.description}`,
     `- ${manifest.routing.description}`,
-    `- **Delegate when:** ${manifest.routing.delegation.when}`,
+    `- **Delegate when:** ${manifest.routing.when}`,
   ].join('\n');
   ].join('\n');
 }
 }
 
 
@@ -1122,28 +1334,6 @@ export function createAgents(
 
 
       const override = getOverrideFromAgents(mergedAgents, name);
       const override = getOverrideFromAgents(mergedAgents, name);
       const inlinePrompt = override?.prompt;
       const inlinePrompt = override?.prompt;
-      const profile = marketplace?.profiles.get(
-        name as keyof typeof ROLE_DEFINITIONS,
-      );
-      if (profile && name in ROLE_DEFINITIONS) {
-        applyOverrides(agent, boundedPackageOverride(profile.manifest));
-        applyMarketplaceCapabilities(agent, profile);
-        agent.config.prompt = composePackagePrompt(
-          agent.config.prompt ?? '',
-          profile.manifest.instructions,
-          profile.manifest.instructionMode,
-        );
-        if (
-          marketplace &&
-          (inlinePrompt !== undefined || customPrompts.prompt)
-        ) {
-          marketplace.diagnostics.push({
-            packageId: profile.packageId,
-            code: 'prompt-masked',
-            message: `User prompt replacement masks marketplace profile ${profile.packageId} on @${name}`,
-          });
-        }
-      }
       const defaultPrompt = agent.config.prompt ?? '';
       const defaultPrompt = agent.config.prompt ?? '';
 
 
       agent.config.prompt = resolvePrompt(
       agent.config.prompt = resolvePrompt(
@@ -1216,41 +1406,67 @@ export function createAgents(
     (activated) => {
     (activated) => {
       const name = activated.manifest.agentName;
       const name = activated.manifest.agentName;
       if (disabled.has(name)) return [];
       if (disabled.has(name)) return [];
-      const role = ROLE_DEFINITIONS[activated.manifest.baseRole];
+      const role = activated.manifest.extends
+        ? ROLE_DEFINITIONS[activated.manifest.extends.builtin]
+        : undefined;
       const override = getOverrideFromAgents(mergedAgents, name);
       const override = getOverrideFromAgents(mergedAgents, name);
       const customPrompts = loadAgentPrompt(name, {
       const customPrompts = loadAgentPrompt(name, {
         preset: runtime.preset,
         preset: runtime.preset,
         projectDirectory: options?.projectDirectory,
         projectDirectory: options?.projectDirectory,
       });
       });
-      const packageModel =
-        typeof boundedPackageOverride(activated.manifest).model === 'string'
-          ? (boundedPackageOverride(activated.manifest).model as string)
-          : undefined;
-      const agent = role.createBaseline(
-        getPrimaryModelFromOverride(override) ??
-          packageModel ??
-          (override?.inheritModelFrom === 'orchestrator'
-            ? configuredOrchestratorModel
-            : primaryModel) ??
-          (role.defaultModel as string),
-      );
+      const policy = activated.manifest.model;
+      const model =
+        policy.source === 'explicit'
+          ? typeof policy.candidates[0] === 'string'
+            ? policy.candidates[0]
+            : policy.candidates[0]?.id
+          : policy.source === 'builtin'
+            ? role?.defaultModel
+            : policy.source === 'orchestrator'
+              ? (configuredOrchestratorModel ?? primaryModel)
+              : undefined;
+      const agent: AgentDefinition = role
+        ? role.createBaseline(
+            (model ?? role.defaultModel ?? DEFAULT_MODELS.oracle) as string,
+          )
+        : {
+            name,
+            description: activated.manifest.description,
+            config: { prompt: activated.manifest.prompt },
+          };
       agent.name = name;
       agent.name = name;
-      agent.baseRole = role.id;
-      agent.description =
-        activated.manifest.overrides.description ??
-        activated.manifest.description ??
-        role.description;
-      applyOverrides(agent, boundedPackageOverride(activated.manifest));
-      applyMarketplaceCapabilities(agent, activated);
+      if (role) agent.baseRole = role.id;
+      agent.description = activated.manifest.description;
+      if (policy.source === 'session') delete agent.config.model;
+      if (policy.source === 'explicit') {
+        agent._modelArray = policy.candidates.map((candidate) => ({
+          ...(typeof candidate === 'string' ? { id: candidate } : candidate),
+        }));
+        agent.config.model = model;
+        if (
+          typeof policy.candidates[0] !== 'string' &&
+          policy.candidates[0]?.variant
+        ) {
+          agent.config.variant = policy.candidates[0].variant;
+        }
+      }
+      if (activated.manifest.temperature !== undefined) {
+        agent.config.temperature = activated.manifest.temperature;
+      }
+      if (activated.manifest.color !== undefined) {
+        agent.config.color = activated.manifest.color;
+      }
       agent.config.prompt = resolvePrompt(
       agent.config.prompt = resolvePrompt(
         name,
         name,
         override?.prompt,
         override?.prompt,
         customPrompts.prompt,
         customPrompts.prompt,
-        composePackagePrompt(
-          role.basePrompt,
-          activated.manifest.instructions,
-          'append',
-        ),
+        role
+          ? composePackagePrompt(
+              role.basePrompt,
+              activated.manifest.prompt,
+              activated.manifest.extends?.promptMode ?? 'append',
+            )
+          : activated.manifest.prompt,
         customPrompts.appendPrompt,
         customPrompts.appendPrompt,
         [TASK_REJECTION_INSTRUCTION],
         [TASK_REJECTION_INSTRUCTION],
       );
       );
@@ -1300,11 +1516,7 @@ export function createAgents(
     applyModelInheritance(agent, override, configuredOrchestratorModel);
     applyModelInheritance(agent, override, configuredOrchestratorModel);
     applyFinalPermissions(
     applyFinalPermissions(
       agent,
       agent,
-      marketplaceSkillList(
-        agent,
-        override,
-        extraMarketplaceSkills(marketplace, agent.name, agent.baseRole),
-      ),
+      marketplaceSkillList(agent, override, undefined),
       runtime.disabledSkills,
       runtime.disabledSkills,
     );
     );
     return agent;
     return agent;
@@ -1352,13 +1564,16 @@ export function createAgents(
       }
       }
     }
     }
     applyModelInheritance(agent, override, configuredOrchestratorModel);
     applyModelInheritance(agent, override, configuredOrchestratorModel);
+    const marketplaceEntry = marketplacePackage(marketplace, agent.name);
+    const marketplaceCeilings = marketplaceEntry
+      ? marketplaceCapabilityCeilings(agent, marketplaceEntry, override)
+      : undefined;
+    if (marketplaceCeilings) {
+      applyMarketplaceCapabilities(agent, marketplaceCeilings);
+    }
     applyFinalPermissions(
     applyFinalPermissions(
       agent,
       agent,
-      marketplaceSkillList(
-        agent,
-        override,
-        extraMarketplaceSkills(marketplace, agent.name, agent.baseRole),
-      ),
+      marketplaceCeilings?.skills,
       runtime.disabledSkills,
       runtime.disabledSkills,
     );
     );
     return [agent];
     return [agent];
@@ -1552,21 +1767,25 @@ function buildCanonicalAgentConfigs(
   };
   };
 
 
   const resolveAgentMcps = (agent: AgentDefinition): string[] => {
   const resolveAgentMcps = (agent: AgentDefinition): string[] => {
-    const configured = runtime.agent(agent.name)?.mcps;
-    if (configured !== undefined) {
-      return getAgentMcpList(agent.name, runtime);
-    }
-    const extras = extraMarketplaceMcps(
+    const marketplaceEntry = marketplacePackage(
       options?.marketplace,
       options?.marketplace,
       agent.name,
       agent.name,
-      agent.baseRole,
     );
     );
-    if (agent.baseRole) {
-      return uniqueNames([
-        ...ROLE_DEFINITIONS[agent.baseRole].defaultMcps,
-        ...extras,
-      ]);
+    if (marketplaceEntry) {
+      return [
+        ...marketplaceCapabilityCeilings(
+          agent,
+          marketplaceEntry,
+          getOverrideFromAgents(runtime.agents(), agent.name),
+        ).mcps,
+      ];
     }
     }
+    const configured = runtime.agent(agent.name)?.mcps;
+    if (configured !== undefined) {
+      return getAgentMcpList(agent.name, runtime);
+    }
+    if (agent.baseRole)
+      return [...ROLE_DEFINITIONS[agent.baseRole].defaultMcps];
     return getAgentMcpList(agent.name, runtime);
     return getAgentMcpList(agent.name, runtime);
   };
   };
 
 
@@ -1698,8 +1917,12 @@ export function buildResolvedAgentRegistry(
   const modelArrays = cloneOwned(runtime.modelArrays);
   const modelArrays = cloneOwned(runtime.modelArrays);
   for (const activated of marketplace.agents) {
   for (const activated of marketplace.agents) {
     const name = activated.manifest.agentName;
     const name = activated.manifest.agentName;
-    if (modelArrays[name]) continue;
     const agent = agents.find((entry) => entry.name === name);
     const agent = agents.find((entry) => entry.name === name);
+    if (agent?._modelArray && agent._modelArray.length > 0) {
+      modelArrays[name] = cloneOwned(agent._modelArray);
+      continue;
+    }
+    if (modelArrays[name]) continue;
     const model =
     const model =
       typeof agent?.config.model === 'string' ? agent.config.model : undefined;
       typeof agent?.config.model === 'string' ? agent.config.model : undefined;
     if (model) {
     if (model) {
@@ -1723,28 +1946,80 @@ export function buildResolvedAgentRegistry(
     const hostConfig = hostAgentFor(runtime, runtimeName, name);
     const hostConfig = hostAgentFor(runtime, runtimeName, name);
     const hostModel = resolvePrimaryModelValue(hostConfig?.model);
     const hostModel = resolvePrimaryModelValue(hostConfig?.model);
     const configuredModels = modelArrays[name];
     const configuredModels = modelArrays[name];
-    if (
+    const hostVariant = stringVariant(hostConfig?.variant);
+    const hostReplacesModel = Boolean(
       hostModel &&
       hostModel &&
-      configuredModels?.[0]?.id &&
-      configuredModels[0].id !== hostModel
-    ) {
+        configuredModels?.[0]?.id &&
+        configuredModels[0].id !== hostModel,
+    );
+    if (hostReplacesModel && hostModel) {
+      const override = getOverrideFromAgents(runtime.agents(), name);
+      const ownerVariant = hasExplicitVariantOverride(override)
+        ? stringVariant((rawConfig as Record<string, unknown>).variant)
+        : undefined;
       modelArrays[name] = [
       modelArrays[name] = [
         {
         {
           id: hostModel,
           id: hostModel,
-          ...(typeof hostConfig?.variant === 'string'
-            ? { variant: hostConfig.variant }
-            : {}),
+          ...(hostVariant !== undefined
+            ? { variant: hostVariant }
+            : ownerVariant !== undefined
+              ? { variant: ownerVariant }
+              : {}),
         },
         },
       ];
       ];
+    } else if (
+      hostModel &&
+      hostVariant !== undefined &&
+      configuredModels?.[0]?.id === hostModel
+    ) {
+      modelArrays[name] = configuredModels.map((entry, index) =>
+        index === 0 ? { ...entry, variant: hostVariant } : entry,
+      );
     }
     }
     const ownedConfig = cloneOwned(rawConfig) as SDKAgentConfig &
     const ownedConfig = cloneOwned(rawConfig) as SDKAgentConfig &
       Record<string, unknown>;
       Record<string, unknown>;
     mergeSupportedHostAgentFields(ownedConfig, hostConfig);
     mergeSupportedHostAgentFields(ownedConfig, hostConfig);
-    ownedConfig.permission = projectPermissionValues(
-      name,
-      rawConfig.permission,
-      hostConfig?.permission,
-    ) as SDKAgentConfig['permission'];
+    if (hostReplacesModel && hostVariant === undefined) {
+      const override = getOverrideFromAgents(runtime.agents(), name);
+      if (!hasExplicitVariantOverride(override)) {
+        delete ownedConfig.variant;
+      }
+    }
+    const marketplaceEntry = marketplacePackage(marketplace, name);
+    if (marketplaceEntry) {
+      const ceilings = marketplaceCapabilityCeilings(
+        agent,
+        marketplaceEntry,
+        getOverrideFromAgents(runtime.agents(), name),
+      );
+      ownedConfig.permission = projectMarketplacePermission(
+        rawConfig.permission,
+        hostConfig?.permission,
+        ceilings.tools,
+        ceilings.skills,
+        ceilings.mcps,
+        options?.availableMcpNames ?? [],
+        hostConfig?.tools,
+      ) as SDKAgentConfig['permission'];
+      if (
+        hostConfig?.tools &&
+        typeof hostConfig.tools === 'object' &&
+        !Array.isArray(hostConfig.tools)
+      ) {
+        ownedConfig.tools = Object.fromEntries(
+          Object.entries(hostConfig.tools as Record<string, unknown>).filter(
+            ([tool, value]) =>
+              ceilings.tools.includes(tool) && typeof value === 'boolean',
+          ),
+        ) as Record<string, boolean>;
+      }
+    } else {
+      ownedConfig.permission = projectPermissionValues(
+        name,
+        rawConfig.permission,
+        hostConfig?.permission,
+      ) as SDKAgentConfig['permission'];
+    }
     sdkConfigs[name] = ownedConfig;
     sdkConfigs[name] = ownedConfig;
   }
   }
 
 
@@ -1752,6 +2027,31 @@ export function buildResolvedAgentRegistry(
   // config hook must not repeat this resolution against its host projection.
   // config hook must not repeat this resolution against its host projection.
   applyModelInheritanceToConfig(sdkConfigs as Record<string, unknown>, runtime);
   applyModelInheritanceToConfig(sdkConfigs as Record<string, unknown>, runtime);
 
 
+  // Inheritance replaces any lower-layer candidate chain. Rebuild the
+  // orchestrator-inherited single candidate from the finalized SDK config so
+  // the fallback surface cannot retain a package model or variant.
+  for (const agent of agents) {
+    const override = getOverrideFromAgents(runtime.agents(), agent.name);
+    if (override?.inheritModelFrom === 'session') {
+      delete modelArrays[agent.name];
+      continue;
+    }
+    if (override?.inheritModelFrom !== 'orchestrator') continue;
+    const config = sdkConfigs[agent.name];
+    const model = resolvePrimaryModelValue(config?.model);
+    if (model === undefined) {
+      delete modelArrays[agent.name];
+      continue;
+    }
+    const variant = stringVariant(config?.variant);
+    modelArrays[agent.name] = [
+      {
+        id: model,
+        ...(variant !== undefined ? { variant } : {}),
+      },
+    ];
+  }
+
   const mcpLists: Record<string, readonly string[]> = {};
   const mcpLists: Record<string, readonly string[]> = {};
   const skillPermissions: Record<
   const skillPermissions: Record<
     string,
     string,
@@ -1799,6 +2099,9 @@ export function buildResolvedAgentRegistry(
       availableMcpNames,
       availableMcpNames,
     ) as SDKAgentConfig['permission'];
     ) as SDKAgentConfig['permission'];
     const permission = sdkConfig.permission;
     const permission = sdkConfig.permission;
+    const marketplaceAgent = marketplace.agents.find(
+      (entry) => entry.manifest.agentName === agent.name,
+    );
     skillPermissions[agent.name] =
     skillPermissions[agent.name] =
       typeof permission === 'object' &&
       typeof permission === 'object' &&
       permission !== null &&
       permission !== null &&
@@ -1807,34 +2110,25 @@ export function buildResolvedAgentRegistry(
         ? cloneOwned(
         ? cloneOwned(
             permission.skill as Record<string, 'allow' | 'ask' | 'deny'>,
             permission.skill as Record<string, 'allow' | 'ask' | 'deny'>,
           )
           )
-        : getSkillPermissionsForAgent(
-            agent.name,
-            runtime.agent(agent.name)?.skills,
-            runtime.disabledSkills,
-          );
-    const marketplaceAgent = marketplace.agents.find(
-      (entry) => entry.manifest.agentName === agent.name,
-    );
-    const marketplaceProfile = agent.baseRole
-      ? marketplace.profiles.get(agent.baseRole)
-      : undefined;
+        : marketplaceAgent
+          ? {}
+          : getSkillPermissionsForAgent(
+              agent.name,
+              runtime.agent(agent.name)?.skills,
+              runtime.disabledSkills,
+            );
     provenance[agent.name] = marketplaceAgent
     provenance[agent.name] = marketplaceAgent
       ? `marketplace-agent:${marketplaceAgent.packageId}@${marketplaceAgent.version}`
       ? `marketplace-agent:${marketplaceAgent.packageId}@${marketplaceAgent.version}`
-      : marketplaceProfile && isSubagent(agent.name)
-        ? `marketplace-profile:${marketplaceProfile.packageId}@${marketplaceProfile.version}`
-        : agent.name === 'orchestrator'
-          ? 'orchestrator-special'
-          : isSubagent(agent.name)
-            ? `builtin:${agent.name}`
-            : 'configured-agent';
+      : agent.name === 'orchestrator'
+        ? 'orchestrator-special'
+        : isSubagent(agent.name)
+          ? `builtin:${agent.name}`
+          : 'configured-agent';
     runtimeNameByCanonicalId[agent.name] = agent.name;
     runtimeNameByCanonicalId[agent.name] = agent.name;
     canonicalIdByRuntimeName[agent.name] = agent.name;
     canonicalIdByRuntimeName[agent.name] = agent.name;
     if (marketplaceAgent) {
     if (marketplaceAgent) {
       packageIdByRuntimeName[agent.name] = marketplaceAgent.packageId;
       packageIdByRuntimeName[agent.name] = marketplaceAgent.packageId;
       runtimeNameByPackageId[marketplaceAgent.packageId] = agent.name;
       runtimeNameByPackageId[marketplaceAgent.packageId] = agent.name;
-    } else if (marketplaceProfile && isSubagent(agent.name)) {
-      packageIdByRuntimeName[agent.name] = marketplaceProfile.packageId;
-      runtimeNameByPackageId[marketplaceProfile.packageId] = agent.name;
     }
     }
     if (agent.displayName) {
     if (agent.displayName) {
       const displayName = normalizeAgentName(agent.displayName);
       const displayName = normalizeAgentName(agent.displayName);

+ 0 - 2
src/agents/role-definitions.ts

@@ -17,7 +17,6 @@ export interface RoleDefinition {
   readonly basePrompt: string;
   readonly basePrompt: string;
   readonly description: string;
   readonly description: string;
   readonly derivable: true;
   readonly derivable: true;
-  readonly profileTargetable: true;
   readonly defaultModel: string | undefined;
   readonly defaultModel: string | undefined;
   readonly defaultSkills: readonly string[];
   readonly defaultSkills: readonly string[];
   readonly defaultMcps: readonly string[];
   readonly defaultMcps: readonly string[];
@@ -346,7 +345,6 @@ function defineRole(
     basePrompt,
     basePrompt,
     description,
     description,
     derivable: true,
     derivable: true,
-    profileTargetable: true,
     defaultModel,
     defaultModel,
     defaultSkills: Object.freeze([...defaultSkills]),
     defaultSkills: Object.freeze([...defaultSkills]),
     defaultMcps: Object.freeze([...defaultMcps]),
     defaultMcps: Object.freeze([...defaultMcps]),

+ 0 - 1
src/cli/config-io.ts

@@ -448,7 +448,6 @@ function configMutationPaths(filePath: string): MarketplacePaths {
     packagesDir: join(root, 'packages'),
     packagesDir: join(root, 'packages'),
     lockfilePath: join(root, 'lock.json'),
     lockfilePath: join(root, 'lock.json'),
     lockDir: join(root, 'lock'),
     lockDir: join(root, 'lock'),
-    breakerDir: join(root, 'lock.breaker'),
     stagingDir: join(root, '.staging'),
     stagingDir: join(root, '.staging'),
   };
   };
 }
 }

+ 0 - 2
src/cli/index.ts

@@ -85,8 +85,6 @@ Usage:
   bunx oh-my-opencode-slim marketplace remove <package-id> [--force]
   bunx oh-my-opencode-slim marketplace remove <package-id> [--force]
   bunx oh-my-opencode-slim marketplace enable <package-id>
   bunx oh-my-opencode-slim marketplace enable <package-id>
   bunx oh-my-opencode-slim marketplace disable <package-id>
   bunx oh-my-opencode-slim marketplace disable <package-id>
-  bunx oh-my-opencode-slim marketplace profile <role> <package-id>
-  bunx oh-my-opencode-slim marketplace profile <role> --clear
   bunx oh-my-opencode-slim marketplace status [--json]
   bunx oh-my-opencode-slim marketplace status [--json]
 
 
 Options:
 Options:

+ 1 - 24
src/cli/marketplace.test.ts

@@ -8,7 +8,6 @@ describe('marketplace CLI parsing', () => {
       value: './package.json',
       value: './package.json',
       force: false,
       force: false,
       json: false,
       json: false,
-      clear: false,
     });
     });
     expect(
     expect(
       parseMarketplaceArgs(['import', './package-v2.json', '--update']),
       parseMarketplaceArgs(['import', './package-v2.json', '--update']),
@@ -17,7 +16,6 @@ describe('marketplace CLI parsing', () => {
       value: './package-v2.json',
       value: './package-v2.json',
       force: false,
       force: false,
       json: false,
       json: false,
-      clear: false,
       update: true,
       update: true,
     });
     });
     expect(
     expect(
@@ -27,7 +25,6 @@ describe('marketplace CLI parsing', () => {
       value: 'community/example@1.2.3',
       value: 'community/example@1.2.3',
       force: false,
       force: false,
       json: false,
       json: false,
-      clear: false,
     });
     });
   });
   });
 
 
@@ -39,7 +36,6 @@ describe('marketplace CLI parsing', () => {
       value: undefined,
       value: undefined,
       force: false,
       force: false,
       json: false,
       json: false,
-      clear: false,
     });
     });
     expect(parseMarketplaceArgs(['update', 'community/example']).command).toBe(
     expect(parseMarketplaceArgs(['update', 'community/example']).command).toBe(
       'update',
       'update',
@@ -49,37 +45,18 @@ describe('marketplace CLI parsing', () => {
     ).toThrow();
     ).toThrow();
   });
   });
 
 
-  test('parses enable, disable, and profile commands', () => {
+  test('parses enable, disable, and status commands', () => {
     expect(parseMarketplaceArgs(['enable', 'community/example'])).toEqual({
     expect(parseMarketplaceArgs(['enable', 'community/example'])).toEqual({
       command: 'enable',
       command: 'enable',
       value: 'community/example',
       value: 'community/example',
       force: false,
       force: false,
       json: false,
       json: false,
-      clear: false,
-    });
-    expect(
-      parseMarketplaceArgs(['profile', 'librarian', 'community/deep']),
-    ).toEqual({
-      command: 'profile',
-      role: 'librarian',
-      value: 'community/deep',
-      force: false,
-      json: false,
-      clear: false,
-    });
-    expect(parseMarketplaceArgs(['profile', 'oracle', '--clear'])).toEqual({
-      command: 'profile',
-      role: 'oracle',
-      force: false,
-      json: false,
-      clear: true,
     });
     });
     expect(parseMarketplaceArgs(['status', '--json'])).toEqual({
     expect(parseMarketplaceArgs(['status', '--json'])).toEqual({
       command: 'status',
       command: 'status',
       value: undefined,
       value: undefined,
       force: false,
       force: false,
       json: true,
       json: true,
-      clear: false,
     });
     });
   });
   });
 });
 });

+ 5 - 58
src/cli/marketplace.ts

@@ -5,7 +5,6 @@ import {
 import {
 import {
   disableMarketplacePackage,
   disableMarketplacePackage,
   enableMarketplaceAgent,
   enableMarketplaceAgent,
-  setMarketplaceProfile,
 } from '../marketplace/activation-config';
 } from '../marketplace/activation-config';
 import {
 import {
   collectMarketplaceStatus,
   collectMarketplaceStatus,
@@ -23,16 +22,13 @@ export type MarketplaceCommandName =
   | 'remove'
   | 'remove'
   | 'enable'
   | 'enable'
   | 'disable'
   | 'disable'
-  | 'profile'
   | 'status';
   | 'status';
 
 
 export interface MarketplaceArgs {
 export interface MarketplaceArgs {
   command: MarketplaceCommandName;
   command: MarketplaceCommandName;
   value?: string;
   value?: string;
-  role?: string;
   force: boolean;
   force: boolean;
   json: boolean;
   json: boolean;
-  clear: boolean;
   update?: boolean;
   update?: boolean;
 }
 }
 
 
@@ -47,7 +43,6 @@ function commandSet(): readonly string[] {
     'remove',
     'remove',
     'enable',
     'enable',
     'disable',
     'disable',
-    'profile',
     'status',
     'status',
   ];
   ];
 }
 }
@@ -56,13 +51,12 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
   const rawCommand = args[0];
   const rawCommand = args[0];
   if (!rawCommand || !commandSet().includes(rawCommand)) {
   if (!rawCommand || !commandSet().includes(rawCommand)) {
     throw new Error(
     throw new Error(
-      'Usage: marketplace install|import|list|show|verify|update|remove|enable|disable|profile|status [value] [options]',
+      'Usage: marketplace install|import|list|show|verify|update|remove|enable|disable|status [value] [options]',
     );
     );
   }
   }
   const command = rawCommand as MarketplaceCommandName;
   const command = rawCommand as MarketplaceCommandName;
   const force = args.includes('--force');
   const force = args.includes('--force');
   const json = args.includes('--json');
   const json = args.includes('--json');
-  const clear = args.includes('--clear');
   const update = args.includes('--update');
   const update = args.includes('--update');
   const options = args.filter((arg) => arg.startsWith('--'));
   const options = args.filter((arg) => arg.startsWith('--'));
   const allowedOptions = new Set(
   const allowedOptions = new Set(
@@ -70,11 +64,9 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
       ? ['--force']
       ? ['--force']
       : command === 'verify' || command === 'show' || command === 'status'
       : command === 'verify' || command === 'show' || command === 'status'
         ? ['--json']
         ? ['--json']
-        : command === 'profile'
-          ? ['--clear']
-          : command === 'import'
-            ? ['--update']
-            : [],
+        : command === 'import'
+          ? ['--update']
+          : [],
   );
   );
   for (const option of options) {
   for (const option of options) {
     if (!allowedOptions.has(option)) {
     if (!allowedOptions.has(option)) {
@@ -85,34 +77,6 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
   }
   }
 
 
   const positional = args.slice(1).filter((arg) => !arg.startsWith('--'));
   const positional = args.slice(1).filter((arg) => !arg.startsWith('--'));
-  if (command === 'profile') {
-    if (!positional[0]) {
-      throw new Error('marketplace profile requires a specialist role');
-    }
-    if (clear) {
-      if (positional.length > 1) {
-        throw new Error('marketplace profile --clear accepts only a role');
-      }
-      return {
-        command,
-        role: positional[0],
-        force,
-        json,
-        clear,
-      };
-    }
-    if (!positional[1] || positional.length > 2) {
-      throw new Error('marketplace profile requires a role and package ID');
-    }
-    return {
-      command,
-      role: positional[0],
-      value: positional[1],
-      force,
-      json,
-      clear,
-    };
-  }
   const needsValue = [
   const needsValue = [
     'install',
     'install',
     'import',
     'import',
@@ -147,7 +111,6 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
     value: positional[0],
     value: positional[0],
     force,
     force,
     json,
     json,
-    clear,
     ...(command === 'import' && update ? { update: true } : {}),
     ...(command === 'import' && update ? { update: true } : {}),
   };
   };
 }
 }
@@ -196,7 +159,7 @@ export async function marketplaceCommand(
       case 'list':
       case 'list':
         for (const pkg of service.list()) {
         for (const pkg of service.list()) {
           console.log(
           console.log(
-            `${pkg.manifest.id}@${pkg.manifest.version}\t${pkg.manifest.kind}\t${pkg.manifest.displayName}`,
+            `${pkg.manifest.id}@${pkg.manifest.version}\t${pkg.manifest.displayName}`,
           );
           );
         }
         }
         return 0;
         return 0;
@@ -245,22 +208,6 @@ export async function marketplaceCommand(
           ),
           ),
         );
         );
         return 0;
         return 0;
-      case 'profile':
-        setMarketplaceProfile(
-          projectDir,
-          parsed.role as string,
-          parsed.clear ? null : (parsed.value as string),
-          service.store,
-        );
-        console.log(
-          mutationReloadNotice(
-            parsed.clear
-              ? `Cleared the ${parsed.role} profile`
-              : `Selected ${parsed.value} for ${parsed.role}`,
-            'unknown',
-          ),
-        );
-        return 0;
       case 'status': {
       case 'status': {
         const report = collectMarketplaceStatus({
         const report = collectMarketplaceStatus({
           service,
           service,

+ 1 - 5
src/config/loader.test.ts

@@ -33,12 +33,11 @@ describe('typed agent and preset merge precedence', () => {
     ).toEqual({ explorer: { model: 'higher/model' } });
     ).toEqual({ explorer: { model: 'higher/model' } });
   });
   });
 
 
-  test('preserves omitted activation and applies explicit empty tombstones', () => {
+  test('preserves omitted activation and applies explicit empty activation', () => {
     const base = {
     const base = {
       agents: {},
       agents: {},
       marketplace: {
       marketplace: {
         agents: ['community/one'],
         agents: ['community/one'],
-        profiles: { oracle: 'community/oracle' },
       },
       },
     } satisfies Preset;
     } satisfies Preset;
     expect(mergePreset(base, { agents: {} })).toEqual(base);
     expect(mergePreset(base, { agents: {} })).toEqual(base);
@@ -48,19 +47,16 @@ describe('typed agent and preset merge precedence', () => {
       agents: {},
       agents: {},
       marketplace: {
       marketplace: {
         agents: [],
         agents: [],
-        profiles: { oracle: 'community/oracle' },
       },
       },
     });
     });
     expect(
     expect(
       mergePreset(base, {
       mergePreset(base, {
         agents: {},
         agents: {},
-        marketplace: { profiles: { oracle: null } },
       }),
       }),
     ).toEqual({
     ).toEqual({
       agents: {},
       agents: {},
       marketplace: {
       marketplace: {
         agents: ['community/one'],
         agents: ['community/one'],
-        profiles: { oracle: null },
       },
       },
     });
     });
   });
   });

+ 2 - 9
src/config/loader.ts

@@ -429,10 +429,11 @@ function loadConfigFromPath(
     const result = PluginConfigSchema.safeParse(rawConfig);
     const result = PluginConfigSchema.safeParse(rawConfig);
 
 
     if (!result.success) {
     if (!result.success) {
+      const message = 'Config does not match schema';
       options?.onWarning?.({
       options?.onWarning?.({
         path: configPath,
         path: configPath,
         kind: 'invalid-schema',
         kind: 'invalid-schema',
-        message: 'Config does not match schema',
+        message,
         formatted: result.error.format(),
         formatted: result.error.format(),
       });
       });
       if (!options?.silent) {
       if (!options?.silent) {
@@ -692,14 +693,6 @@ export function mergePreset(
         ...(Object.hasOwn(override.marketplace, 'agents')
         ...(Object.hasOwn(override.marketplace, 'agents')
           ? { agents: override.marketplace.agents }
           ? { agents: override.marketplace.agents }
           : {}),
           : {}),
-        ...(Object.hasOwn(override.marketplace, 'profiles')
-          ? {
-              profiles: {
-                ...(base.marketplace?.profiles ?? {}),
-                ...(override.marketplace.profiles ?? {}),
-              },
-            }
-          : {}),
       }
       }
     : base.marketplace;
     : base.marketplace;
   return {
   return {

+ 4 - 9
src/config/schema.test.ts

@@ -7,12 +7,11 @@ import {
 } from './schema';
 } from './schema';
 
 
 describe('structured preset schema', () => {
 describe('structured preset schema', () => {
-  it('keeps agent overrides separate from reserved marketplace activation', () => {
+  it('keeps agent overrides separate from marketplace activation', () => {
     const result = PresetSchema.safeParse({
     const result = PresetSchema.safeParse({
       agents: { explorer: { model: 'provider/explorer' } },
       agents: { explorer: { model: 'provider/explorer' } },
       marketplace: {
       marketplace: {
         agents: ['community/example'],
         agents: ['community/example'],
-        profiles: { librarian: null },
       },
       },
     });
     });
     expect(result.success).toBe(true);
     expect(result.success).toBe(true);
@@ -47,27 +46,23 @@ describe('structured preset schema', () => {
     }
     }
   });
   });
 
 
-  it('normalizes package IDs and validates profile targets', () => {
+  it('normalizes package IDs', () => {
     const result = PresetSchema.safeParse({
     const result = PresetSchema.safeParse({
       marketplace: {
       marketplace: {
         agents: ['  community/one  '],
         agents: ['  community/one  '],
-        profiles: { oracle: '  community/oracle  ' },
       },
       },
     });
     });
     expect(result.success).toBe(true);
     expect(result.success).toBe(true);
     if (result.success) {
     if (result.success) {
       expect(result.data.marketplace?.agents).toEqual(['community/one']);
       expect(result.data.marketplace?.agents).toEqual(['community/one']);
-      expect(result.data.marketplace?.profiles).toEqual({
-        oracle: 'community/oracle',
-      });
     }
     }
   });
   });
 
 
-  it('rejects duplicate package IDs and unsupported profile targets', () => {
+  it('rejects duplicate package IDs and removed activation fields', () => {
     const result = PresetSchema.safeParse({
     const result = PresetSchema.safeParse({
       marketplace: {
       marketplace: {
         agents: ['community/one', ' community/one '],
         agents: ['community/one', ' community/one '],
-        profiles: { orchestrator: 'community/one' },
+        removed: 'community/one',
       },
       },
     });
     });
     expect(result.success).toBe(false);
     expect(result.success).toBe(false);

+ 0 - 43
src/config/schema.ts

@@ -142,11 +142,6 @@ export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>;
 /** Normalized model entry with optional per-model variant. */
 /** Normalized model entry with optional per-model variant. */
 export type ModelEntry = { id: string; variant?: string };
 export type ModelEntry = { id: string; variant?: string };
 
 
-export const MarketplaceProfileTargetSchema = AgentBaseRoleSchema;
-export type MarketplaceProfileTarget = z.infer<
-  typeof MarketplaceProfileTargetSchema
->;
-
 const MarketplacePackageIdSchema = z.string().trim().min(1);
 const MarketplacePackageIdSchema = z.string().trim().min(1);
 const MarketplacePackageIdsSchema = z
 const MarketplacePackageIdsSchema = z
   .array(MarketplacePackageIdSchema)
   .array(MarketplacePackageIdSchema)
@@ -171,44 +166,6 @@ const MarketplacePackageIdsSchema = z
 export const MarketplaceActivationSchema = z
 export const MarketplaceActivationSchema = z
   .object({
   .object({
     agents: MarketplacePackageIdsSchema.optional(),
     agents: MarketplacePackageIdsSchema.optional(),
-    profiles: z
-      .record(z.string(), MarketplacePackageIdSchema.nullable())
-      .superRefine((profiles, ctx) => {
-        const packageIds = Object.values(profiles).filter(
-          (packageId): packageId is string => packageId !== null,
-        );
-        if (new Set(packageIds).size !== packageIds.length) {
-          ctx.addIssue({
-            code: 'custom',
-            message: 'Profile package IDs must be unique',
-          });
-        }
-        for (const target of Object.keys(profiles)) {
-          if (!MarketplaceProfileTargetSchema.safeParse(target).success) {
-            ctx.addIssue({
-              code: 'custom',
-              path: [target],
-              message: `Unsupported profile target '${target}'`,
-            });
-          }
-        }
-      })
-      .optional(),
-  })
-  .superRefine((activation, ctx) => {
-    const packageIds = [
-      ...(activation.agents ?? []),
-      ...Object.values(activation.profiles ?? {}).filter(
-        (packageId): packageId is string => packageId !== null,
-      ),
-    ];
-    if (new Set(packageIds).size !== packageIds.length) {
-      ctx.addIssue({
-        code: 'custom',
-        path: ['agents'],
-        message: 'Activation package IDs must be unique',
-      });
-    }
   })
   })
   .strict();
   .strict();
 
 

+ 28 - 45
src/marketplace-contract/index.ts

@@ -12,6 +12,7 @@ import {
   MARKETPLACE_DIGEST_DOMAIN,
   MARKETPLACE_DIGEST_DOMAIN,
   MARKETPLACE_MANIFEST_SCHEMA_VERSION,
   MARKETPLACE_MANIFEST_SCHEMA_VERSION,
   MarketplaceAgentManifestSchema,
   MarketplaceAgentManifestSchema,
+  MarketplaceAgentManifestSummarySchema,
   type MarketplaceDigest,
   type MarketplaceDigest,
   MarketplaceDigestSchema,
   MarketplaceDigestSchema,
   type MarketplacePackageBundle,
   type MarketplacePackageBundle,
@@ -20,7 +21,6 @@ import {
   MarketplacePackageIdSchema,
   MarketplacePackageIdSchema,
   type MarketplacePackageManifest,
   type MarketplacePackageManifest,
   MarketplacePackageManifestSchema,
   MarketplacePackageManifestSchema,
-  MarketplaceProfileManifestSchema,
   type MarketplaceVersion,
   type MarketplaceVersion,
   MarketplaceVersionSchema,
   MarketplaceVersionSchema,
 } from '../marketplace/schemas';
 } from '../marketplace/schemas';
@@ -44,26 +44,17 @@ export {
   MarketplacePackageBundleSchema,
   MarketplacePackageBundleSchema,
   MarketplacePackageIdSchema,
   MarketplacePackageIdSchema,
   MarketplacePackageManifestSchema,
   MarketplacePackageManifestSchema,
-  MarketplaceProfileManifestSchema,
   MarketplaceVersionSchema,
   MarketplaceVersionSchema,
 };
 };
 
 
-export const MARKETPLACE_REGISTRY_SCHEMA_VERSION = 2 as const;
+export const MARKETPLACE_REGISTRY_SCHEMA_VERSION = 3 as const;
 export const DEFAULT_MARKETPLACE_REGISTRY_URL =
 export const DEFAULT_MARKETPLACE_REGISTRY_URL =
-  'https://registry.ohmyopencodeslim.com/v1/' as const;
+  'https://registry.ohmyopencodeslim.com/v2/' as const;
 
 
-const MarketplaceAgentSummarySchema = MarketplaceAgentManifestSchema.omit({
-  instructions: true,
-});
-const MarketplaceProfileSummarySchema = MarketplaceProfileManifestSchema.omit({
-  instructions: true,
-});
+const MarketplaceAgentSummarySchema = MarketplaceAgentManifestSummarySchema;
 
 
-/** Public catalog metadata; package instructions never enter the index. */
-export const MarketplaceManifestSummarySchema = z.discriminatedUnion('kind', [
-  MarketplaceAgentSummarySchema,
-  MarketplaceProfileSummarySchema,
-]);
+/** Public catalog metadata; package prompts never enter the index. */
+export const MarketplaceManifestSummarySchema = MarketplaceAgentSummarySchema;
 
 
 export const MarketplaceRegistryEntrySchema = z
 export const MarketplaceRegistryEntrySchema = z
   .object({
   .object({
@@ -142,15 +133,13 @@ function validateRegistryEntries(
   }
   }
 }
 }
 
 
-const MarketplaceRegistryIndexV1Schema = z
-  .object({
-    schemaVersion: z.literal(1),
-    entries: z.array(MarketplaceRegistryEntrySchema).max(100_000),
-  })
-  .strict()
-  .superRefine((index, ctx) => validateRegistryEntries(index.entries, ctx));
+const CANONICAL_MARKETPLACE_RETIREMENT_IDS = [
+  'alvin/deepwork-implementer',
+  'alvin/deepwork-recon',
+  'alvin/deepwork-reviewer',
+] as const;
 
 
-const MarketplaceRegistryIndexV2Schema = z
+const MarketplaceRegistryIndexV3Schema = z
   .object({
   .object({
     schemaVersion: z.literal(MARKETPLACE_REGISTRY_SCHEMA_VERSION),
     schemaVersion: z.literal(MARKETPLACE_REGISTRY_SCHEMA_VERSION),
     entries: z.array(MarketplaceRegistryEntrySchema).max(100_000),
     entries: z.array(MarketplaceRegistryEntrySchema).max(100_000),
@@ -159,7 +148,6 @@ const MarketplaceRegistryIndexV2Schema = z
   .strict()
   .strict()
   .superRefine((index, ctx) => {
   .superRefine((index, ctx) => {
     validateRegistryEntries(index.entries, ctx);
     validateRegistryEntries(index.entries, ctx);
-    const entryIds = new Set(index.entries.map((entry) => entry.id));
     const seen = new Set<string>();
     const seen = new Set<string>();
     for (let position = 0; position < index.retirements.length; position += 1) {
     for (let position = 0; position < index.retirements.length; position += 1) {
       const retirement = index.retirements[position];
       const retirement = index.retirements[position];
@@ -171,13 +159,6 @@ const MarketplaceRegistryIndexV2Schema = z
         });
         });
       }
       }
       seen.add(retirement.id);
       seen.add(retirement.id);
-      if (!entryIds.has(retirement.id)) {
-        ctx.addIssue({
-          code: 'custom',
-          path: ['retirements', position, 'id'],
-          message: `Marketplace retirement ${retirement.id} has no registry entry`,
-        });
-      }
       const previous = index.retirements[position - 1];
       const previous = index.retirements[position - 1];
       if (
       if (
         previous &&
         previous &&
@@ -190,12 +171,18 @@ const MarketplaceRegistryIndexV2Schema = z
         });
         });
       }
       }
     }
     }
+    for (const id of CANONICAL_MARKETPLACE_RETIREMENT_IDS) {
+      if (!seen.has(id)) {
+        ctx.addIssue({
+          code: 'custom',
+          path: ['retirements'],
+          message: `Registry must contain canonical retirement ${id}`,
+        });
+      }
+    }
   });
   });
 
 
-export const MarketplaceRegistryIndexSchema = z.discriminatedUnion(
-  'schemaVersion',
-  [MarketplaceRegistryIndexV1Schema, MarketplaceRegistryIndexV2Schema],
-);
+export const MarketplaceRegistryIndexSchema = MarketplaceRegistryIndexV3Schema;
 
 
 export type MarketplaceManifestSummary = z.infer<
 export type MarketplaceManifestSummary = z.infer<
   typeof MarketplaceManifestSummarySchema
   typeof MarketplaceManifestSummarySchema
@@ -225,7 +212,7 @@ export function projectMarketplaceManifestSummary(
   manifest: MarketplacePackageManifest,
   manifest: MarketplacePackageManifest,
 ): MarketplaceManifestSummary {
 ): MarketplaceManifestSummary {
   const parsed = MarketplacePackageBundleSchema.shape.manifest.parse(manifest);
   const parsed = MarketplacePackageBundleSchema.shape.manifest.parse(manifest);
-  const { instructions: _instructions, ...summary } = parsed;
+  const { prompt: _prompt, ...summary } = parsed;
   return summary as MarketplaceManifestSummary;
   return summary as MarketplaceManifestSummary;
 }
 }
 
 
@@ -251,7 +238,9 @@ export function createMarketplaceRegistryEntry(
 
 
 export function createMarketplaceRegistryIndex(
 export function createMarketplaceRegistryIndex(
   entries: readonly MarketplaceRegistryEntry[],
   entries: readonly MarketplaceRegistryEntry[],
-  retirements: readonly MarketplaceRegistryRetirement[] = [],
+  retirements: readonly MarketplaceRegistryRetirement[] = CANONICAL_MARKETPLACE_RETIREMENT_IDS.map(
+    (id) => ({ id }),
+  ),
 ): MarketplaceRegistryIndex {
 ): MarketplaceRegistryIndex {
   const sorted = [...entries].sort(
   const sorted = [...entries].sort(
     (left, right) =>
     (left, right) =>
@@ -272,9 +261,7 @@ export function createMarketplaceRegistryIndex(
 export function retiredMarketplaceRegistryIds(
 export function retiredMarketplaceRegistryIds(
   index: MarketplaceRegistryIndex,
   index: MarketplaceRegistryIndex,
 ): ReadonlySet<string> {
 ): ReadonlySet<string> {
-  return new Set(
-    index.schemaVersion === 2 ? index.retirements.map(({ id }) => id) : [],
-  );
+  return new Set(index.retirements.map(({ id }) => id));
 }
 }
 
 
 export function isMarketplaceRegistryIdRetired(
 export function isMarketplaceRegistryIdRetired(
@@ -363,7 +350,7 @@ export function parseMarketplaceRegistrySelector(
 export function resolveMarketplaceRegistryEntry(
 export function resolveMarketplaceRegistryEntry(
   index: MarketplaceRegistryIndex,
   index: MarketplaceRegistryIndex,
   selector: MarketplaceRegistrySelector,
   selector: MarketplaceRegistrySelector,
-  compatibility: { pluginVersion: string; roleContractVersion: string },
+  compatibility: { pluginVersion: string },
   minimumVersion?: string,
   minimumVersion?: string,
 ): MarketplaceRegistryEntry {
 ): MarketplaceRegistryEntry {
   if (
   if (
@@ -382,10 +369,6 @@ export function resolveMarketplaceRegistryEntry(
       satisfies(
       satisfies(
         compatibility.pluginVersion,
         compatibility.pluginVersion,
         entry.summary.compatibility.plugin,
         entry.summary.compatibility.plugin,
-      ) &&
-      satisfies(
-        compatibility.roleContractVersion,
-        entry.summary.compatibility.roleContract,
       ),
       ),
   );
   );
   const selected = [...candidates].sort(
   const selected = [...candidates].sort(

+ 2 - 97
src/marketplace/activation-config.ts

@@ -1,8 +1,4 @@
 import { mutateJsonFile } from '../cli/config-io';
 import { mutateJsonFile } from '../cli/config-io';
-import {
-  type SpecialistRole,
-  SUPPORTED_SPECIALIST_ROLES,
-} from '../config/agent-roles';
 import {
 import {
   findPluginConfigPaths,
   findPluginConfigPaths,
   loadPluginConfig,
   loadPluginConfig,
@@ -57,7 +53,6 @@ function cloneActivation(
 ): MarketplaceActivation {
 ): MarketplaceActivation {
   return {
   return {
     ...(activation?.agents ? { agents: [...activation.agents] } : {}),
     ...(activation?.agents ? { agents: [...activation.agents] } : {}),
-    ...(activation?.profiles ? { profiles: { ...activation.profiles } } : {}),
   };
   };
 }
 }
 
 
@@ -78,27 +73,6 @@ function activationOverlay(
   if (parent === undefined || !samePackageIds(parent.agents, desired.agents)) {
   if (parent === undefined || !samePackageIds(parent.agents, desired.agents)) {
     overlay.agents = [...(desired.agents ?? [])];
     overlay.agents = [...(desired.agents ?? [])];
   }
   }
-  const parentProfiles = parent?.profiles ?? {};
-  const desiredProfiles = desired.profiles ?? {};
-  const profiles: NonNullable<MarketplaceActivation['profiles']> = {
-    ...parentProfiles,
-  };
-  let profilesChanged = false;
-  for (const key of new Set([
-    ...Object.keys(parentProfiles),
-    ...Object.keys(desiredProfiles),
-  ])) {
-    const role = key as SpecialistRole;
-    const next = Object.hasOwn(desiredProfiles, key)
-      ? desiredProfiles[role]
-      : undefined;
-    const previous = parentProfiles[role];
-    if (next !== previous) {
-      profiles[role] = next ?? null;
-      profilesChanged = true;
-    }
-  }
-  if (profilesChanged) overlay.profiles = profiles;
   return overlay;
   return overlay;
 }
 }
 
 
@@ -144,26 +118,13 @@ export function enableMarketplaceAgent(
 ): void {
 ): void {
   const id = normalizeMarketplacePackageId(packageId);
   const id = normalizeMarketplacePackageId(packageId);
   assertMarketplacePackageNotRetired(id);
   assertMarketplacePackageNotRetired(id);
-  const pkg = store.show(id);
-  if (pkg.manifest.kind !== 'agent') {
-    throw new MarketplaceActivationError(
-      `${id} is a ${pkg.manifest.kind} package; use profile to activate it`,
-    );
-  }
+  store.show(id);
   const config = loadPluginConfig(directory, { silent: true });
   const config = loadPluginConfig(directory, { silent: true });
   const presetName = activePresetName(config);
   const presetName = activePresetName(config);
   persistActivation(directory, presetName, (activation) => {
   persistActivation(directory, presetName, (activation) => {
     const agents = [...(activation.agents ?? [])];
     const agents = [...(activation.agents ?? [])];
     if (!agents.includes(id)) agents.push(id);
     if (!agents.includes(id)) agents.push(id);
-    const profiles = { ...(activation.profiles ?? {}) };
-    for (const [role, value] of Object.entries(profiles)) {
-      if (value === id) {
-        throw new MarketplaceActivationError(
-          `${id} is already selected as the ${role} profile`,
-        );
-      }
-    }
-    return { agents, profiles };
+    return { agents };
   });
   });
 }
 }
 
 
@@ -176,61 +137,5 @@ export function disableMarketplacePackage(
   const presetName = activePresetName(config);
   const presetName = activePresetName(config);
   persistActivation(directory, presetName, (activation) => ({
   persistActivation(directory, presetName, (activation) => ({
     agents: (activation.agents ?? []).filter((value) => value !== id),
     agents: (activation.agents ?? []).filter((value) => value !== id),
-    profiles: Object.fromEntries(
-      Object.entries(activation.profiles ?? {}).map(([role, value]) => [
-        role,
-        value === id ? null : value,
-      ]),
-    ),
   }));
   }));
 }
 }
-
-export function setMarketplaceProfile(
-  directory: string,
-  role: string,
-  packageId: string | null,
-  store = new MarketplaceStore(),
-): void {
-  const parsedRole = SUPPORTED_SPECIALIST_ROLES.find((name) => name === role);
-  if (!parsedRole) {
-    throw new MarketplaceActivationError(
-      `Unsupported profile target '${role}'`,
-    );
-  }
-  const config = loadPluginConfig(directory, { silent: true });
-  const presetName = activePresetName(config);
-  if (packageId === null) {
-    persistActivation(directory, presetName, (activation) => ({
-      agents: [...(activation.agents ?? [])],
-      profiles: { ...(activation.profiles ?? {}), [parsedRole]: null },
-    }));
-    return;
-  }
-  const id = normalizeMarketplacePackageId(packageId);
-  assertMarketplacePackageNotRetired(id);
-  const pkg = store.show(id);
-  if (pkg.manifest.kind !== 'profile') {
-    throw new MarketplaceActivationError(
-      `${id} is a ${pkg.manifest.kind} package; use enable to activate it`,
-    );
-  }
-  if (pkg.manifest.targetRole !== parsedRole) {
-    throw new MarketplaceActivationError(
-      `${id} targets ${pkg.manifest.targetRole}, not ${parsedRole}`,
-    );
-  }
-  persistActivation(directory, presetName, (activation) => {
-    const agents = [...(activation.agents ?? [])];
-    if (agents.includes(id)) {
-      throw new MarketplaceActivationError(
-        `${id} is already enabled as a marketplace agent`,
-      );
-    }
-    return {
-      agents,
-      profiles: { ...(activation.profiles ?? {}), [parsedRole]: id },
-    };
-  });
-}
-
-export type { SpecialistRole };

+ 64 - 315
src/marketplace/activation.test.ts

@@ -22,7 +22,6 @@ import { resolveRuntimeAgentName } from '../utils/agent-variant';
 import {
 import {
   disableMarketplacePackage,
   disableMarketplacePackage,
   enableMarketplaceAgent,
   enableMarketplaceAgent,
-  setMarketplaceProfile,
 } from './activation-config';
 } from './activation-config';
 import type { MarketplacePackageBundle } from './schemas';
 import type { MarketplacePackageBundle } from './schemas';
 import { MarketplaceStore } from './store';
 import { MarketplaceStore } from './store';
@@ -39,76 +38,29 @@ function agentBundle(
 ): MarketplacePackageBundle {
 ): MarketplacePackageBundle {
   return {
   return {
     manifest: {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/docs-researcher',
       id: 'community/docs-researcher',
       version: '1.0.0',
       version: '1.0.0',
-      kind: 'agent',
       displayName: 'Docs researcher',
       displayName: 'Docs researcher',
       description: 'A derived explorer.',
       description: 'A derived explorer.',
-      instructions: 'Prefer documentation paths first.',
+      agentName: 'docsresearcher',
+      prompt: 'Prefer documentation paths first.',
       author: { name: 'Community' },
       author: { name: 'Community' },
       tags: ['docs'],
       tags: ['docs'],
       license: 'MIT',
       license: 'MIT',
       compatibility: {
       compatibility: {
-        plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-        roleContract: '^1.0.0',
+        plugin: '>=3.0.0-beta.3 <4.0.0',
       },
       },
       routing: {
       routing: {
         description: 'Research docs and examples.',
         description: 'Research docs and examples.',
         keywords: ['docs'],
         keywords: ['docs'],
-        delegation: {
-          when: 'When docs research is needed.',
-          preferredRoles: [],
-        },
-      },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
+        when: 'When docs research is needed.',
       },
       },
-      capabilities: { tools: [], permissions: [] },
-      baseRole: 'explorer',
-      agentName: 'docsresearcher',
-      overrides: {},
-      ...overrides,
-    } as MarketplacePackageBundle['manifest'],
-  };
-}
-
-function profileBundle(
-  overrides: Partial<MarketplacePackageBundle['manifest']> = {},
-): MarketplacePackageBundle {
-  return {
-    manifest: {
-      schemaVersion: 1,
-      id: 'community/deep-explorer',
-      version: '1.0.0',
-      kind: 'profile',
-      displayName: 'Deep explorer',
-      description: 'A specialist profile.',
-      instructions: 'Search more exhaustively than usual.',
-      author: { name: 'Community' },
-      tags: ['profile'],
-      license: 'MIT',
-      compatibility: {
-        plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-        roleContract: '^1.0.0',
-      },
-      routing: {
-        description: 'Deep exploration profile.',
-        keywords: ['deep'],
-        delegation: {
-          when: 'When exhaustive search is needed.',
-          preferredRoles: [],
-        },
-      },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
-      },
-      capabilities: { tools: [], permissions: [] },
-      targetRole: 'explorer',
-      instructionMode: 'append',
-      overrides: {},
+      skills: [],
+      mcps: [],
+      tools: [],
+      model: { source: 'explicit', candidates: ['provider/model'] },
+      extends: { builtin: 'explorer', promptMode: 'append' },
       ...overrides,
       ...overrides,
     } as MarketplacePackageBundle['manifest'],
     } as MarketplacePackageBundle['manifest'],
   };
   };
@@ -188,7 +140,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          overrides: { model: 'package/model', description: 'Package desc' },
+          model: { source: 'explicit', candidates: ['package/model'] },
+          description: 'Package desc',
         }),
         }),
       );
       );
       const registry = registryFor(
       const registry = registryFor(
@@ -234,101 +187,6 @@ describe('marketplace runtime activation', () => {
     }
     }
   });
   });
 
 
-  test('appends and replaces specialist profile instructions', () => {
-    const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
-    try {
-      const store = new MarketplaceStore({ rootDir: root });
-      store.install(profileBundle());
-      const appended = registryFor(
-        {
-          preset: 'work',
-          presets: {
-            work: {
-              agents: {},
-              marketplace: {
-                profiles: { explorer: 'community/deep-explorer' },
-              },
-            },
-          },
-        },
-        store,
-      );
-      const explorer = appended.agents.find(
-        (agent) => agent.name === 'explorer',
-      );
-      expect(explorer?.config.prompt).toContain(
-        ROLE_DEFINITIONS.explorer.basePrompt,
-      );
-      expect(explorer?.config.prompt).toContain(
-        'Search more exhaustively than usual.',
-      );
-      expect(appended.provenance.explorer).toBe(
-        'marketplace-profile:community/deep-explorer@1.0.0',
-      );
-
-      store.remove('community/deep-explorer');
-      store.install(
-        profileBundle({
-          id: 'community/replace-explorer',
-          instructionMode: 'replace',
-          instructions: 'Replacement explorer instructions.',
-        }),
-      );
-      const replaced = registryFor(
-        {
-          preset: 'work',
-          presets: {
-            work: {
-              agents: {},
-              marketplace: {
-                profiles: { explorer: 'community/replace-explorer' },
-              },
-            },
-          },
-        },
-        store,
-        'marketplace-replace-test',
-      );
-      const replacedExplorer = replaced.agents.find(
-        (agent) => agent.name === 'explorer',
-      );
-      expect(replacedExplorer?.config.prompt).toContain(
-        'Replacement explorer instructions.',
-      );
-      expect(replacedExplorer?.config.prompt).not.toContain(
-        'You are Explorer - a fast codebase navigation specialist.',
-      );
-    } finally {
-      rmSync(root, { recursive: true, force: true });
-    }
-  });
-
-  test('respects profile tombstones and does not create a second agent', () => {
-    const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
-    try {
-      const store = new MarketplaceStore({ rootDir: root });
-      store.install(profileBundle());
-      const registry = registryFor(
-        {
-          preset: 'work',
-          presets: {
-            work: {
-              agents: {},
-              marketplace: { profiles: { explorer: null } },
-            },
-          },
-        },
-        store,
-      );
-      expect(
-        registry.agents.filter((agent) => agent.baseRole === 'explorer'),
-      ).toHaveLength(1);
-      expect(registry.provenance.explorer).toBe('builtin:explorer');
-    } finally {
-      rmSync(root, { recursive: true, force: true });
-    }
-  });
-
   test('rejects colliding runtime names', () => {
   test('rejects colliding runtime names', () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     try {
     try {
@@ -362,10 +220,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: ['not-a-real-skill'], optional: ['simplify'] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['not-a-real-skill'],
+          mcps: [],
         }),
         }),
       );
       );
       const registry = registryFor(
       const registry = registryFor(
@@ -395,10 +251,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: ['not-a-real-skill'] },
-            mcps: { required: [], optional: ['unknown-mcp'] },
-          },
+          skills: [],
+          mcps: [],
         }),
         }),
       );
       );
       const registry = registryFor(
       const registry = registryFor(
@@ -569,10 +423,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['context7'], optional: [] },
-          },
+          skills: [],
+          mcps: ['context7'],
         }),
         }),
       );
       );
       RuntimeConfig.reset('marketplace-mcp-projection');
       RuntimeConfig.reset('marketplace-mcp-projection');
@@ -616,10 +468,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['injected-plugin-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['injected-plugin-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset('marketplace-injected-mcp');
       RuntimeConfig.reset('marketplace-injected-mcp');
@@ -665,10 +515,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: [],
         }),
         }),
       );
       );
       RuntimeConfig.reset('marketplace-disk-skill');
       RuntimeConfig.reset('marketplace-disk-skill');
@@ -736,10 +584,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['injected-host-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['injected-host-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset('marketplace-host-mcp');
       RuntimeConfig.reset('marketplace-host-mcp');
@@ -792,10 +638,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['project-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset(project);
       RuntimeConfig.reset(project);
@@ -838,10 +682,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['project-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset(project);
       RuntimeConfig.reset(project);
@@ -901,10 +743,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: ['project-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset(project);
       RuntimeConfig.reset(project);
@@ -962,10 +802,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['empty-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['empty-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset(project);
       RuntimeConfig.reset(project);
@@ -987,10 +825,8 @@ describe('marketplace runtime activation', () => {
         agentBundle({
         agentBundle({
           id: 'community/config-json',
           id: 'community/config-json',
           agentName: 'configjson',
           agentName: 'configjson',
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['config-json-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['config-json-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset(`${project}-ok`);
       RuntimeConfig.reset(`${project}-ok`);
@@ -1023,10 +859,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: ['hidden-skill'], optional: [] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['hidden-skill'],
+          mcps: [],
         }),
         }),
       );
       );
       RuntimeConfig.reset(root);
       RuntimeConfig.reset(root);
@@ -1058,10 +892,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: ['project-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset('marketplace-explicit-disabled');
       RuntimeConfig.reset('marketplace-explicit-disabled');
@@ -1089,19 +921,18 @@ describe('marketplace runtime activation', () => {
     }
     }
   });
   });
 
 
-  test('reserves custom names except the package override key', () => {
+  test('reserves custom names except the marketplace owner override key', () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     try {
     try {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
-      store.install(
-        agentBundle({
-          overrides: { displayName: 'janitor' },
-        }),
-      );
+      store.install(agentBundle());
       const colliding = registryFor(
       const colliding = registryFor(
         {
         {
           preset: 'work',
           preset: 'work',
-          agents: { janitor: { model: 'custom/model' } },
+          agents: {
+            docsresearcher: { displayName: 'janitor' },
+            janitor: { model: 'custom/model' },
+          },
           presets: {
           presets: {
             work: {
             work: {
               agents: { janitor: { model: 'custom/model' } },
               agents: { janitor: { model: 'custom/model' } },
@@ -1114,10 +945,7 @@ describe('marketplace runtime activation', () => {
       expect(colliding.diagnostics[0]?.code).toBe('collision');
       expect(colliding.diagnostics[0]?.code).toBe('collision');
 
 
       store.install(
       store.install(
-        agentBundle({
-          id: 'community/janitor',
-          agentName: 'janitor',
-        }),
+        agentBundle({ id: 'community/janitor', agentName: 'janitor' }),
       );
       );
       const ownKey = registryFor(
       const ownKey = registryFor(
         {
         {
@@ -1149,7 +977,7 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          overrides: { displayName: 'docsresearcher' },
+          agentName: 'docsresearcher',
         }),
         }),
       );
       );
       const registry = registryFor(
       const registry = registryFor(
@@ -1157,7 +985,7 @@ describe('marketplace runtime activation', () => {
           preset: 'work',
           preset: 'work',
           presets: {
           presets: {
             work: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'docsresearcher' } },
               marketplace: { agents: ['community/docs-researcher'] },
               marketplace: { agents: ['community/docs-researcher'] },
             },
             },
           },
           },
@@ -1203,10 +1031,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: ['config-mcp'], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: ['config-mcp'],
         }),
         }),
       );
       );
       RuntimeConfig.reset(root);
       RuntimeConfig.reset(root);
@@ -1238,58 +1064,17 @@ describe('marketplace runtime activation', () => {
     }
     }
   });
   });
 
 
-  test('disables unsafe profile aliases without affecting other packages', () => {
-    const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
-    try {
-      const store = new MarketplaceStore({ rootDir: root });
-      store.install(agentBundle());
-      store.install(
-        profileBundle({
-          overrides: { displayName: 'Docs Researcher' },
-        }),
-      );
-      const registry = registryFor(
-        {
-          preset: 'work',
-          presets: {
-            work: {
-              agents: {},
-              marketplace: {
-                agents: ['community/docs-researcher'],
-                profiles: { explorer: 'community/deep-explorer' },
-              },
-            },
-          },
-        },
-        store,
-      );
-      expect(
-        registry.agents.some((agent) => agent.name === 'docsresearcher'),
-      ).toBe(true);
-      expect(registry.diagnostics[0]?.code).toBe('invalid-alias');
-      expect(registry.diagnostics[0]?.packageId).toBe(
-        'community/deep-explorer',
-      );
-    } finally {
-      rmSync(root, { recursive: true, force: true });
-    }
-  });
-
   test('disables unsafe display aliases without throwing', () => {
   test('disables unsafe display aliases without throwing', () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     try {
     try {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
-      store.install(
-        agentBundle({
-          overrides: { displayName: 'Docs Researcher' },
-        }),
-      );
+      store.install(agentBundle());
       const registry = registryFor(
       const registry = registryFor(
         {
         {
           preset: 'work',
           preset: 'work',
           presets: {
           presets: {
             work: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'Docs Researcher' } },
               marketplace: { agents: ['community/docs-researcher'] },
               marketplace: { agents: ['community/docs-researcher'] },
             },
             },
           },
           },
@@ -1347,7 +1132,7 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
       store.install(
         agentBundle({
         agentBundle({
-          overrides: { displayName: 'docsalias', model: 'package/model' },
+          model: { source: 'explicit', candidates: ['package/model'] },
         }),
         }),
       );
       );
       const registry = registryFor(
       const registry = registryFor(
@@ -1355,7 +1140,7 @@ describe('marketplace runtime activation', () => {
           preset: 'work',
           preset: 'work',
           presets: {
           presets: {
             work: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'docsalias' } },
               marketplace: { agents: ['community/docs-researcher'] },
               marketplace: { agents: ['community/docs-researcher'] },
             },
             },
           },
           },
@@ -1376,12 +1161,12 @@ describe('marketplace runtime activation', () => {
         expect.objectContaining({
         expect.objectContaining({
           packageId: 'community/docs-researcher',
           packageId: 'community/docs-researcher',
           runtimeName: 'docsalias',
           runtimeName: 'docsalias',
-          kind: 'agent',
         }),
         }),
       ]);
       ]);
       expect(
       expect(
-        (registry.sdkConfigs.docsalias.permission as Record<string, unknown>)
-          .marketplace,
+        (registry.sdkConfigs.docsalias.permission as Record<string, unknown>)[
+          '*'
+        ],
       ).toBe('deny');
       ).toBe('deny');
       expect(
       expect(
         (
         (
@@ -1389,7 +1174,7 @@ describe('marketplace runtime activation', () => {
             string,
             string,
             unknown
             unknown
           >
           >
-        ).marketplace,
+        )['*'],
       ).toBe('deny');
       ).toBe('deny');
       expect(registry.sdkConfigs.docsalias.model).toBe('package/model');
       expect(registry.sdkConfigs.docsalias.model).toBe('package/model');
       expect(registry.sdkConfigs.docsresearcher.model).toBe('package/model');
       expect(registry.sdkConfigs.docsresearcher.model).toBe('package/model');
@@ -1423,7 +1208,6 @@ describe('marketplace runtime activation', () => {
         expect.objectContaining({
         expect.objectContaining({
           packageId: 'community/docs-researcher',
           packageId: 'community/docs-researcher',
           runtimeName: 'fieldscout',
           runtimeName: 'fieldscout',
-          kind: 'agent',
         }),
         }),
       ]);
       ]);
     } finally {
     } finally {
@@ -1453,7 +1237,13 @@ describe('marketplace runtime activation', () => {
     const sessionDir = `${project}\0session`;
     const sessionDir = `${project}\0session`;
     try {
     try {
       const store = new MarketplaceStore({ rootDir: join(root, 'store') });
       const store = new MarketplaceStore({ rootDir: join(root, 'store') });
-      store.install(agentBundle({ overrides: { displayName: 'docsalias' } }));
+      store.install(agentBundle());
+      writeFileSync(
+        join(project, '.opencode', 'oh-my-opencode-slim.json'),
+        JSON.stringify({
+          agents: { docsresearcher: { displayName: 'docsalias' } },
+        }),
+      );
       RuntimeConfig.reset(sessionDir);
       RuntimeConfig.reset(sessionDir);
       const session = RuntimeConfig.init(sessionDir, {
       const session = RuntimeConfig.init(sessionDir, {
         preset: 'session-preset',
         preset: 'session-preset',
@@ -1583,47 +1373,6 @@ describe('marketplace activation persistence', () => {
     }
     }
   });
   });
 
 
-  test('selects and clears a profile for a supported role', () => {
-    const root = mkdtempSync(join(tmpdir(), 'marketplace-config-'));
-    const configHome = join(root, 'config');
-    const project = join(root, 'project');
-    try {
-      process.env.XDG_CONFIG_HOME = configHome;
-      mkdirSync(join(configHome, 'opencode'), { recursive: true });
-      const userConfig = join(
-        configHome,
-        'opencode',
-        'oh-my-opencode-slim.json',
-      );
-      writeFileSync(
-        userConfig,
-        JSON.stringify({
-          preset: 'work',
-          presets: { work: { agents: {} } },
-        }),
-      );
-      const store = new MarketplaceStore({ rootDir: join(root, 'store') });
-      store.install(profileBundle());
-      setMarketplaceProfile(
-        project,
-        'explorer',
-        'community/deep-explorer',
-        store,
-      );
-      expect(
-        JSON.parse(readFileSync(userConfig, 'utf8')).presets.work.marketplace
-          .profiles.explorer,
-      ).toBe('community/deep-explorer');
-      setMarketplaceProfile(project, 'explorer', null, store);
-      expect(
-        JSON.parse(readFileSync(userConfig, 'utf8')).presets.work.marketplace
-          .profiles.explorer,
-      ).toBeNull();
-    } finally {
-      rmSync(root, { recursive: true, force: true });
-    }
-  });
-
   test('competing activation writers keep both package enables', async () => {
   test('competing activation writers keep both package enables', async () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-config-'));
     const root = mkdtempSync(join(tmpdir(), 'marketplace-config-'));
     const configHome = join(root, 'config');
     const configHome = join(root, 'config');

+ 118 - 276
src/marketplace/activation.ts

@@ -1,10 +1,6 @@
-import type { SpecialistRole } from '../config/agent-roles';
 import { AGENT_ALIASES, ALL_AGENT_NAMES } from '../config/constants';
 import { AGENT_ALIASES, ALL_AGENT_NAMES } from '../config/constants';
 import type { RuntimeConfig } from '../config/runtime';
 import type { RuntimeConfig } from '../config/runtime';
-import type {
-  AgentOverrideConfig,
-  MarketplaceActivation,
-} from '../config/schema';
+import type { MarketplaceActivation } from '../config/schema';
 import { isSafeAgentAlias, normalizeAgentName } from '../utils/agent-variant';
 import { isSafeAgentAlias, normalizeAgentName } from '../utils/agent-variant';
 import { validateMarketplaceCompatibility } from './compatibility';
 import { validateMarketplaceCompatibility } from './compatibility';
 import {
 import {
@@ -19,11 +15,7 @@ import {
   discoverPreflightSkills,
   discoverPreflightSkills,
 } from './preflight';
 } from './preflight';
 import { isMarketplacePackageRetired } from './retirements';
 import { isMarketplacePackageRetired } from './retirements';
-import type {
-  MarketplaceAgentManifest,
-  MarketplacePackageManifest,
-  MarketplaceProfileManifest,
-} from './schemas';
+import type { MarketplaceAgentManifest } from './schemas';
 import { MarketplaceStore, type StoredMarketplacePackage } from './store';
 import { MarketplaceStore, type StoredMarketplacePackage } from './store';
 
 
 export {
 export {
@@ -39,11 +31,8 @@ export type MarketplaceDiagnosticCode =
   | 'incompatible'
   | 'incompatible'
   | 'collision'
   | 'collision'
   | 'missing-required-dependency'
   | 'missing-required-dependency'
-  | 'kind-mismatch'
-  | 'target-mismatch'
-  | 'target-disabled'
-  | 'prompt-masked'
   | 'invalid-alias'
   | 'invalid-alias'
+  | 'invalid-capability'
   | 'retired';
   | 'retired';
 
 
 export interface MarketplaceDiagnostic {
 export interface MarketplaceDiagnostic {
@@ -61,18 +50,8 @@ export interface ActivatedMarketplaceAgent {
   requiredMcps: readonly string[];
   requiredMcps: readonly string[];
 }
 }
 
 
-export interface ActivatedMarketplaceProfile {
-  packageId: string;
-  version: string;
-  digest: string;
-  manifest: MarketplaceProfileManifest;
-  requiredSkills: readonly string[];
-  requiredMcps: readonly string[];
-}
-
 export interface MarketplaceActivationPlan {
 export interface MarketplaceActivationPlan {
   agents: ActivatedMarketplaceAgent[];
   agents: ActivatedMarketplaceAgent[];
-  profiles: Map<SpecialistRole, ActivatedMarketplaceProfile>;
   diagnostics: MarketplaceDiagnostic[];
   diagnostics: MarketplaceDiagnostic[];
 }
 }
 
 
@@ -86,7 +65,7 @@ export interface ResolveMarketplaceActivationOptions {
 }
 }
 
 
 export function emptyMarketplaceActivationPlan(): MarketplaceActivationPlan {
 export function emptyMarketplaceActivationPlan(): MarketplaceActivationPlan {
-  return { agents: [], profiles: new Map(), diagnostics: [] };
+  return { agents: [], diagnostics: [] };
 }
 }
 
 
 export function marketplaceActivationFromRuntime(
 export function marketplaceActivationFromRuntime(
@@ -97,33 +76,15 @@ export function marketplaceActivationFromRuntime(
   return runtime.plugin?.presets?.[presetName]?.marketplace;
   return runtime.plugin?.presets?.[presetName]?.marketplace;
 }
 }
 
 
+/** Compose only the package-owned extension layers. */
 export function composePackagePrompt(
 export function composePackagePrompt(
-  rolePrompt: string,
-  instructions: string,
+  builtinPrompt: string,
+  packagePrompt: string,
   mode: 'append' | 'replace',
   mode: 'append' | 'replace',
 ): string {
 ): string {
-  return mode === 'replace' ? instructions : `${rolePrompt}\n\n${instructions}`;
-}
-
-export function boundedPackageOverride(
-  manifest: MarketplacePackageManifest,
-): AgentOverrideConfig {
-  const overrides = manifest.overrides;
-  const result: AgentOverrideConfig = {};
-  if (overrides.model) result.model = overrides.model;
-  if (overrides.variant) result.variant = overrides.variant;
-  if (overrides.temperature !== undefined) {
-    result.temperature = overrides.temperature;
-  }
-  if (overrides.displayName) {
-    const displayName = normalizeAgentName(overrides.displayName);
-    const selfAlias =
-      (manifest.kind === 'agent' && displayName === manifest.agentName) ||
-      (manifest.kind === 'profile' && displayName === manifest.targetRole);
-    if (displayName && !selfAlias) result.displayName = displayName;
-  }
-  if (overrides.description) result.description = overrides.description;
-  return result;
+  return mode === 'replace'
+    ? packagePrompt
+    : `${builtinPrompt}\n\n${packagePrompt}`;
 }
 }
 
 
 export function reservedRuntimeNames(
 export function reservedRuntimeNames(
@@ -145,12 +106,10 @@ export function reservedRuntimeNames(
     }
     }
   }
   }
   for (const name of runtime.customAgentNames) {
   for (const name of runtime.customAgentNames) {
-    if (name === ownOverrideKey) continue;
-    reserved.add(name);
+    if (name !== ownOverrideKey) reserved.add(name);
   }
   }
   for (const [name, override] of Object.entries(runtime.agents())) {
   for (const [name, override] of Object.entries(runtime.agents())) {
-    if (name === ownOverrideKey) continue;
-    if (override.displayName) {
+    if (name !== ownOverrideKey && override.displayName) {
       reserved.add(normalizeAgentName(override.displayName));
       reserved.add(normalizeAgentName(override.displayName));
     }
     }
   }
   }
@@ -165,13 +124,6 @@ function diagnostic(
   return { packageId, code, message };
   return { packageId, code, message };
 }
 }
 
 
-function missingRequired(
-  required: readonly string[],
-  available: ReadonlySet<string>,
-): string[] {
-  return required.filter((name) => !available.has(name));
-}
-
 function marketplaceLoadFailureCode(error: unknown): MarketplaceDiagnosticCode {
 function marketplaceLoadFailureCode(error: unknown): MarketplaceDiagnosticCode {
   if (
   if (
     error instanceof MarketplaceIntegrityError &&
     error instanceof MarketplaceIntegrityError &&
@@ -188,59 +140,80 @@ function marketplaceLoadFailureCode(error: unknown): MarketplaceDiagnosticCode {
   return 'operational';
   return 'operational';
 }
 }
 
 
-function claimDisplayAlias(
+function missing(
+  values: readonly string[],
+  available: ReadonlySet<string>,
+): string[] {
+  return values.filter((value) => !available.has(value));
+}
+
+function claimName(
   packageId: string,
   packageId: string,
-  runtimeName: string,
-  rawDisplayName: string | undefined,
+  name: string,
   reserved: ReadonlySet<string>,
   reserved: ReadonlySet<string>,
-  claimedNames: Set<string>,
+  claimed: Set<string>,
 ): MarketplaceDiagnostic | undefined {
 ): MarketplaceDiagnostic | undefined {
-  if (!rawDisplayName) return undefined;
-  const displayName = normalizeAgentName(rawDisplayName);
-  if (!displayName || displayName === runtimeName) return undefined;
-  if (!isSafeAgentAlias(displayName)) {
+  if (!isSafeAgentAlias(name)) {
     return diagnostic(
     return diagnostic(
       packageId,
       packageId,
       'invalid-alias',
       'invalid-alias',
-      `${packageId} display alias '${rawDisplayName}' is not a valid agent alias`,
+      `${packageId} agentName '${name}' is not a valid agent alias`,
     );
     );
   }
   }
-  if (reserved.has(displayName) || claimedNames.has(displayName)) {
+  if (reserved.has(name) || claimed.has(name)) {
     return diagnostic(
     return diagnostic(
       packageId,
       packageId,
       'collision',
       'collision',
-      `${packageId} display alias '${displayName}' collides with an existing agent name`,
+      `${packageId} runtime name '${name}' collides with an existing agent name`,
     );
     );
   }
   }
-  claimedNames.add(displayName);
   return undefined;
   return undefined;
 }
 }
 
 
+const READONLY_TOOLS = new Set([
+  'read',
+  'glob',
+  'grep',
+  'ast_grep_search',
+  'webfetch',
+  'websearch',
+]);
+
+function validateExtensionCapabilities(
+  pkg: StoredMarketplacePackage,
+  diagnostics: MarketplaceDiagnostic[],
+): boolean {
+  const extension = pkg.manifest.extends;
+  if (!extension) return true;
+  if (extension.builtin !== 'designer' && extension.builtin !== 'fixer') {
+    const invalid = pkg.manifest.tools.filter(
+      (tool) => !READONLY_TOOLS.has(tool),
+    );
+    if (invalid.length > 0) {
+      diagnostics.push(
+        diagnostic(
+          pkg.manifest.id,
+          'invalid-capability',
+          `${pkg.manifest.id} extends read-only builtin ${extension.builtin} and cannot request ${invalid.join(', ')}`,
+        ),
+      );
+      return false;
+    }
+  }
+  return true;
+}
+
 export function resolveMarketplaceActivation(
 export function resolveMarketplaceActivation(
   options: ResolveMarketplaceActivationOptions,
   options: ResolveMarketplaceActivationOptions,
 ): MarketplaceActivationPlan {
 ): MarketplaceActivationPlan {
   const activation = marketplaceActivationFromRuntime(options.runtime);
   const activation = marketplaceActivationFromRuntime(options.runtime);
-  if (!activation) return emptyMarketplaceActivationPlan();
-
-  const agentIds = [...(activation.agents ?? [])].map((id) =>
-    id.trim().toLowerCase(),
-  );
-  const profileEntries = Object.entries(activation.profiles ?? {})
-    .filter((entry): entry is [SpecialistRole, string] => {
-      return entry[1] !== null && entry[1].trim().length > 0;
-    })
-    .map(
-      ([role, packageId]) => [role, packageId.trim().toLowerCase()] as const,
-    );
-
-  if (agentIds.length === 0 && profileEntries.length === 0) {
-    return emptyMarketplaceActivationPlan();
-  }
+  const ids = [
+    ...new Set((activation?.agents ?? []).map((id) => id.trim().toLowerCase())),
+  ];
+  if (ids.length === 0) return emptyMarketplaceActivationPlan();
 
 
   const diagnostics: MarketplaceDiagnostic[] = [];
   const diagnostics: MarketplaceDiagnostic[] = [];
   const store = options.store ?? new MarketplaceStore();
   const store = options.store ?? new MarketplaceStore();
-  const disabledSkills = new Set(options.runtime.disabledSkills);
-  const disabledMcps = new Set(options.runtime.disabledMcps);
   const skills = new Set(
   const skills = new Set(
     (
     (
       options.availableSkillNames ??
       options.availableSkillNames ??
@@ -249,42 +222,21 @@ export function resolveMarketplaceActivation(
         options.projectDirectory,
         options.projectDirectory,
         options.extraSkillDirectories,
         options.extraSkillDirectories,
       )
       )
-    ).filter((name) => !disabledSkills.has(name)),
+    ).filter((name) => !options.runtime.disabledSkills.includes(name)),
   );
   );
   const mcps = new Set(
   const mcps = new Set(
     (
     (
       options.availableMcpNames ??
       options.availableMcpNames ??
       discoverPreflightMcps(options.runtime, options.projectDirectory)
       discoverPreflightMcps(options.runtime, options.projectDirectory)
-    ).filter((name) => !disabledMcps.has(name)),
+    ).filter((name) => !options.runtime.disabledMcps.includes(name)),
   );
   );
-  const claimedNames = new Set<string>();
-  const agents: ActivatedMarketplaceAgent[] = [];
-  const profiles = new Map<SpecialistRole, ActivatedMarketplaceProfile>();
-
-  const selectedIds: string[] = [];
-  for (const packageId of new Set([
-    ...agentIds,
-    ...profileEntries.map(([, packageId]) => packageId),
-  ])) {
-    try {
-      selectedIds.push(normalizeMarketplacePackageId(packageId));
-    } catch {
-      diagnostics.push(
-        diagnostic(packageId, 'missing', `${packageId} is not installed`),
-      );
-    }
-  }
-
-  let selectedPackages = new Map<string, StoredMarketplacePackage>();
-  let selectedErrors = new Map<string, Error>();
-  let selectedLoadFailed = false;
-  const reportedRetired = new Set<string>();
+  const selected = new Map<string, StoredMarketplacePackage>();
+  const errors = new Map<string, Error>();
   try {
   try {
-    const selected = store.loadSelected(selectedIds);
-    selectedPackages = selected.packages;
-    selectedErrors = selected.errors;
+    const loaded = store.loadSelected(ids);
+    for (const [id, pkg] of loaded.packages) selected.set(id, pkg);
+    for (const [id, error] of loaded.errors) errors.set(id, error);
   } catch (error) {
   } catch (error) {
-    selectedLoadFailed = true;
     diagnostics.push(
     diagnostics.push(
       diagnostic(
       diagnostic(
         '(store)',
         '(store)',
@@ -292,199 +244,89 @@ export function resolveMarketplaceActivation(
         error instanceof Error ? error.message : String(error),
         error instanceof Error ? error.message : String(error),
       ),
       ),
     );
     );
+    return { agents: [], diagnostics };
   }
   }
 
 
-  const load = (packageId: string): StoredMarketplacePackage | undefined => {
-    if (selectedLoadFailed) return undefined;
-    let normalized: string;
+  const claimed = new Set<string>();
+  const agents: ActivatedMarketplaceAgent[] = [];
+  for (const packageId of [...ids].sort()) {
+    let id: string;
     try {
     try {
-      normalized = normalizeMarketplacePackageId(packageId);
+      id = normalizeMarketplacePackageId(packageId);
     } catch {
     } catch {
-      return undefined;
-    }
-    if (isMarketplacePackageRetired(normalized)) {
-      if (!reportedRetired.has(normalized)) {
-        reportedRetired.add(normalized);
-        diagnostics.push(
-          diagnostic(
-            normalized,
-            'retired',
-            `${normalized} is retired and will not be activated`,
-          ),
-        );
-      }
-      return undefined;
-    }
-    const error = selectedErrors.get(normalized);
-    if (error) {
       diagnostics.push(
       diagnostics.push(
-        diagnostic(
-          normalized,
-          marketplaceLoadFailureCode(error),
-          error.message,
-        ),
-      );
-      return undefined;
-    }
-    const pkg = selectedPackages.get(normalized);
-    if (!pkg) {
-      diagnostics.push(
-        diagnostic(normalized, 'missing', `${normalized} is not installed`),
-      );
-      return undefined;
-    }
-    try {
-      validateMarketplaceCompatibility(pkg.manifest);
-    } catch (compatError) {
-      const message =
-        compatError instanceof MarketplaceCompatibilityError
-          ? compatError.message
-          : compatError instanceof Error
-            ? compatError.message
-            : String(compatError);
-      diagnostics.push(diagnostic(normalized, 'incompatible', message));
-      return undefined;
-    }
-    return pkg;
-  };
-
-  const preflight = (
-    pkg: StoredMarketplacePackage,
-  ): { skills: string[]; mcps: string[] } | undefined => {
-    const missingSkills = missingRequired(
-      pkg.manifest.requirements.skills.required,
-      skills,
-    );
-    const missingMcps = missingRequired(
-      pkg.manifest.requirements.mcps.required,
-      mcps,
-    );
-    if (missingSkills.length > 0 || missingMcps.length > 0) {
-      diagnostics.push(
-        diagnostic(
-          pkg.manifest.id,
-          'missing-required-dependency',
-          `${pkg.manifest.id} is disabled: missing required ${[
-            ...missingSkills.map((name) => `skill ${name}`),
-            ...missingMcps.map((name) => `mcp ${name}`),
-          ].join(', ')}`,
-        ),
+        diagnostic(packageId, 'missing', `${packageId} is not installed`),
       );
       );
-      return undefined;
+      continue;
     }
     }
-    return {
-      skills: pkg.manifest.requirements.skills.required,
-      mcps: pkg.manifest.requirements.mcps.required,
-    };
-  };
-
-  for (const packageId of [...agentIds].sort()) {
-    const pkg = load(packageId);
-    if (!pkg) continue;
-    if (pkg.manifest.kind !== 'agent') {
+    if (isMarketplacePackageRetired(id)) {
       diagnostics.push(
       diagnostics.push(
-        diagnostic(
-          pkg.manifest.id,
-          'kind-mismatch',
-          `${pkg.manifest.id} is a ${pkg.manifest.kind} package and cannot be activated as an agent`,
-        ),
+        diagnostic(id, 'retired', `${id} is retired and will not be activated`),
       );
       );
       continue;
       continue;
     }
     }
-    const deps = preflight(pkg);
-    if (!deps) continue;
-    const runtimeName = pkg.manifest.agentName;
-    const reserved = reservedRuntimeNames(options.runtime, runtimeName);
-    if (reserved.has(runtimeName) || claimedNames.has(runtimeName)) {
+    const error = errors.get(id);
+    if (error) {
       diagnostics.push(
       diagnostics.push(
-        diagnostic(
-          pkg.manifest.id,
-          'collision',
-          `${pkg.manifest.id} runtime name '${runtimeName}' collides with an existing agent, alias, or display name`,
-        ),
+        diagnostic(id, marketplaceLoadFailureCode(error), error.message),
       );
       );
       continue;
       continue;
     }
     }
-    const aliasError = claimDisplayAlias(
-      pkg.manifest.id,
-      runtimeName,
-      pkg.manifest.overrides.displayName,
-      reserved,
-      claimedNames,
-    );
-    if (aliasError) {
-      diagnostics.push(aliasError);
-      continue;
-    }
-    claimedNames.add(runtimeName);
-    agents.push({
-      packageId: pkg.manifest.id,
-      version: pkg.manifest.version,
-      digest: pkg.digest,
-      manifest: pkg.manifest,
-      requiredSkills: deps.skills,
-      requiredMcps: deps.mcps,
-    });
-  }
-
-  for (const [role, packageId] of profileEntries.sort(([left], [right]) =>
-    left.localeCompare(right),
-  )) {
-    const pkg = load(packageId);
-    if (!pkg) continue;
-    if (pkg.manifest.kind !== 'profile') {
-      diagnostics.push(
-        diagnostic(
-          pkg.manifest.id,
-          'kind-mismatch',
-          `${pkg.manifest.id} is a ${pkg.manifest.kind} package and cannot be activated as a profile`,
-        ),
-      );
+    const pkg = selected.get(id);
+    if (!pkg) {
+      diagnostics.push(diagnostic(id, 'missing', `${id} is not installed`));
       continue;
       continue;
     }
     }
-    if (pkg.manifest.targetRole !== role) {
+    try {
+      validateMarketplaceCompatibility(pkg.manifest);
+    } catch (error) {
       diagnostics.push(
       diagnostics.push(
         diagnostic(
         diagnostic(
-          pkg.manifest.id,
-          'target-mismatch',
-          `${pkg.manifest.id} targets ${pkg.manifest.targetRole}, not ${role}`,
+          id,
+          'incompatible',
+          error instanceof MarketplaceCompatibilityError
+            ? error.message
+            : String(error),
         ),
         ),
       );
       );
       continue;
       continue;
     }
     }
-    if (options.runtime.disabledAgents.has(role)) {
+    if (!validateExtensionCapabilities(pkg, diagnostics)) continue;
+    const missingSkills = missing(pkg.manifest.skills, skills);
+    const missingMcps = missing(pkg.manifest.mcps, mcps);
+    if (missingSkills.length || missingMcps.length) {
       diagnostics.push(
       diagnostics.push(
         diagnostic(
         diagnostic(
-          pkg.manifest.id,
-          'target-disabled',
-          `${pkg.manifest.id} is disabled because @${role} is disabled`,
+          id,
+          'missing-required-dependency',
+          `${id} is disabled: missing ${[
+            ...missingSkills.map((name) => `skill ${name}`),
+            ...missingMcps.map((name) => `mcp ${name}`),
+          ].join(', ')}`,
         ),
         ),
       );
       );
       continue;
       continue;
     }
     }
-    const deps = preflight(pkg);
-    if (!deps) continue;
-    const reserved = reservedRuntimeNames(options.runtime, role);
-    const aliasError = claimDisplayAlias(
-      pkg.manifest.id,
-      role,
-      pkg.manifest.overrides.displayName,
-      reserved,
-      claimedNames,
+    const name = pkg.manifest.agentName;
+    const nameError = claimName(
+      id,
+      name,
+      reservedRuntimeNames(options.runtime, name),
+      claimed,
     );
     );
-    if (aliasError) {
-      diagnostics.push(aliasError);
+    if (nameError) {
+      diagnostics.push(nameError);
       continue;
       continue;
     }
     }
-    profiles.set(role, {
-      packageId: pkg.manifest.id,
+    claimed.add(name);
+    agents.push({
+      packageId: id,
       version: pkg.manifest.version,
       version: pkg.manifest.version,
       digest: pkg.digest,
       digest: pkg.digest,
       manifest: pkg.manifest,
       manifest: pkg.manifest,
-      requiredSkills: deps.skills,
-      requiredMcps: deps.mcps,
+      requiredSkills: pkg.manifest.skills,
+      requiredMcps: pkg.manifest.mcps,
     });
     });
   }
   }
-
-  return { agents, profiles, diagnostics };
+  return { agents, diagnostics };
 }
 }

+ 679 - 0
src/marketplace/agents-only.test.ts

@@ -0,0 +1,679 @@
+import { describe, expect, test } from 'bun:test';
+import { mkdtempSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { buildResolvedAgentRegistry } from '../agents';
+import { RuntimeConfig } from '../config/runtime';
+import { PresetSchema } from '../config/schema';
+import { adaptPermissions } from '../v2/adapters';
+import {
+  composePackagePrompt,
+  resolveMarketplaceActivation,
+} from './activation';
+import { RETIRED_MARKETPLACE_PACKAGE_IDS } from './retirements';
+import {
+  MarketplaceAgentManifestSchema,
+  MarketplacePackageBundleSchema,
+} from './schemas';
+import { MarketplaceStore } from './store';
+
+const manifest = {
+  schemaVersion: 2,
+  id: 'community/example',
+  version: '1.0.0',
+  displayName: 'Example agent',
+  description: 'An example marketplace agent.',
+  agentName: 'example-agent',
+  prompt: 'Package instructions.',
+  routing: {
+    description: 'Example work.',
+    when: 'When appropriate.',
+    keywords: ['example'],
+  },
+  skills: ['simplify'],
+  mcps: ['context7'],
+  tools: ['read', 'webfetch'],
+  author: { name: 'Community' },
+  tags: ['example'],
+  license: 'MIT',
+  compatibility: { plugin: '>=3.0.0-beta.3 <4.0.0' },
+  model: {
+    source: 'explicit',
+    candidates: ['provider/model-a', 'provider/model-b'],
+  },
+} as const;
+
+function bundle(overrides: Record<string, unknown> = {}) {
+  return { manifest: { ...manifest, ...overrides } };
+}
+
+describe('agents-only marketplace contract', () => {
+  test('requires v2 complete manifests and rejects removed fields', () => {
+    expect(MarketplaceAgentManifestSchema.safeParse(manifest).success).toBe(
+      true,
+    );
+    expect(
+      MarketplacePackageBundleSchema.safeParse({
+        manifest: { ...manifest, legacyField: true },
+      }).success,
+    ).toBe(false);
+    expect(
+      MarketplacePackageBundleSchema.safeParse({
+        manifest: {
+          ...manifest,
+          compatibility: { plugin: '>=3.0.0', legacyContract: '^1.0.0' },
+        },
+      }).success,
+    ).toBe(false);
+    expect(
+      PresetSchema.safeParse({
+        marketplace: { agents: ['community/example'], legacyField: {} },
+      }).success,
+    ).toBe(false);
+  });
+
+  test('rejects builtin model policy without an extension', () => {
+    expect(
+      MarketplaceAgentManifestSchema.safeParse({
+        ...manifest,
+        model: { source: 'builtin' },
+      }).success,
+    ).toBe(false);
+  });
+
+  test('composes extension prompts in append and replace modes', () => {
+    expect(
+      composePackagePrompt('Builtin prompt.', 'Package prompt.', 'append'),
+    ).toBe('Builtin prompt.\n\nPackage prompt.');
+    expect(
+      composePackagePrompt('Builtin prompt.', 'Package prompt.', 'replace'),
+    ).toBe('Package prompt.');
+  });
+
+  test('uses exact declared capabilities and keeps readonly extensions readonly', () => {
+    const root = mkdtempSync(join(tmpdir(), 'marketplace-v2-'));
+    try {
+      const store = new MarketplaceStore({
+        rootDir: root,
+        pluginVersion: '3.0.0-beta.3',
+      });
+      store.install(
+        bundle({
+          id: 'community/standalone',
+          agentName: 'standalone',
+          skills: [],
+          mcps: [],
+          tools: ['read'],
+        }),
+      );
+      store.install(
+        bundle({
+          id: 'community/derived',
+          agentName: 'derived',
+          skills: [],
+          mcps: [],
+          tools: ['read'],
+          extends: { builtin: 'explorer', promptMode: 'append' },
+          model: { source: 'builtin' },
+        }),
+      );
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: {},
+            marketplace: {
+              agents: ['community/standalone', 'community/derived'],
+            },
+          },
+        },
+      });
+      const registry = buildResolvedAgentRegistry(runtime, {
+        marketplaceStore: store,
+        availableMcpNames: ['context7'],
+      });
+      expect(registry.mcpLists.standalone).toEqual([]);
+      expect(registry.sdkConfigs.standalone.permission?.read).toBe('allow');
+      expect(registry.sdkConfigs.standalone.permission?.bash).toBe('deny');
+      expect(registry.modelArrays.standalone).toEqual([
+        { id: 'provider/model-a' },
+        { id: 'provider/model-b' },
+      ]);
+      expect(registry.sdkConfigs.derived.permission?.edit).toBe('deny');
+      expect(
+        registry.agents.find((agent) => agent.name === 'explorer'),
+      ).toBeDefined();
+      expect(registry.provenance.explorer).toBe('builtin:explorer');
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+
+  test('owner model overrides replace package fallback chains', () => {
+    const cases = [
+      {
+        owner: { model: 'owner/selected' },
+        expectedModel: 'owner/selected',
+        expectedChain: [{ id: 'owner/selected' }],
+      },
+      {
+        owner: { inheritModelFrom: 'session' as const },
+        expectedModel: undefined,
+        expectedChain: undefined,
+      },
+      {
+        owner: { inheritModelFrom: 'orchestrator' as const },
+        orchestrator: { model: 'owner/orchestrator' },
+        expectedModel: 'owner/orchestrator',
+        expectedChain: [{ id: 'owner/orchestrator' }],
+      },
+    ] as const;
+
+    for (const current of cases) {
+      const root = mkdtempSync(join(tmpdir(), 'marketplace-model-'));
+      try {
+        const store = new MarketplaceStore({
+          rootDir: root,
+          pluginVersion: '3.0.0-beta.3',
+        });
+        store.install(
+          bundle({
+            id: 'community/model-chain',
+            agentName: 'model-chain',
+            skills: [],
+            mcps: [],
+            tools: ['read'],
+          }),
+        );
+        RuntimeConfig.reset(root);
+        const runtime = RuntimeConfig.init(root, {
+          preset: 'work',
+          presets: {
+            work: {
+              agents: {
+                'model-chain': current.owner,
+                ...(current.orchestrator
+                  ? { orchestrator: current.orchestrator }
+                  : {}),
+              },
+              marketplace: { agents: ['community/model-chain'] },
+            },
+          },
+        });
+        const registry = buildResolvedAgentRegistry(runtime, {
+          marketplaceStore: store,
+          availableMcpNames: [],
+        });
+        expect(registry.sdkConfigs['model-chain'].model).toBe(
+          current.expectedModel,
+        );
+        expect(registry.modelArrays['model-chain']).toEqual(
+          current.expectedChain,
+        );
+      } finally {
+        rmSync(root, { recursive: true, force: true });
+      }
+    }
+  });
+
+  test(
+    'model replacements clear package variants and keep surfaces aligned',
+    () => {
+      const cases = [
+        {
+          owner: { model: 'owner/scalar' },
+          expectedModel: 'owner/scalar',
+          expectedVariant: undefined,
+          expectedChain: [{ id: 'owner/scalar' }],
+        },
+        {
+          owner: { model: 'owner/scalar', variant: 'owner-scalar' },
+          expectedModel: 'owner/scalar',
+          expectedVariant: 'owner-scalar',
+          expectedChain: [{ id: 'owner/scalar', variant: 'owner-scalar' }],
+        },
+        {
+          owner: {
+            model: [
+              { id: 'owner/array', variant: 'owner-array' },
+              'owner/fallback',
+            ],
+          },
+          expectedModel: 'owner/array',
+          expectedVariant: 'owner-array',
+          expectedChain: [
+            { id: 'owner/array', variant: 'owner-array' },
+            { id: 'owner/fallback' },
+          ],
+        },
+        {
+          owner: { model: ['owner/array', 'owner/fallback'] },
+          expectedModel: 'owner/array',
+          expectedVariant: undefined,
+          expectedChain: [{ id: 'owner/array' }, { id: 'owner/fallback' }],
+        },
+        {
+          owner: { inheritModelFrom: 'session' as const },
+          expectedModel: undefined,
+          expectedVariant: undefined,
+          expectedChain: undefined,
+        },
+        {
+          owner: {
+            inheritModelFrom: 'session' as const,
+            variant: 'owner-session',
+          },
+          expectedModel: undefined,
+          expectedVariant: 'owner-session',
+          expectedChain: undefined,
+        },
+        {
+          owner: { inheritModelFrom: 'orchestrator' as const },
+          orchestrator: { model: 'owner/orchestrator' },
+          expectedModel: 'owner/orchestrator',
+          expectedVariant: undefined,
+          expectedChain: [{ id: 'owner/orchestrator' }],
+        },
+        {
+          owner: {
+            inheritModelFrom: 'orchestrator' as const,
+            variant: 'owner-orchestrator',
+          },
+          orchestrator: { model: 'owner/orchestrator' },
+          expectedModel: 'owner/orchestrator',
+          expectedVariant: 'owner-orchestrator',
+          expectedChain: [
+            { id: 'owner/orchestrator', variant: 'owner-orchestrator' },
+          ],
+        },
+        {
+          owner: {},
+          host: { model: 'host/replacement' },
+          expectedModel: 'host/replacement',
+          expectedVariant: undefined,
+          expectedChain: [{ id: 'host/replacement' }],
+        },
+        {
+          owner: {},
+          host: { model: 'host/replacement', variant: 'host-variant' },
+          expectedModel: 'host/replacement',
+          expectedVariant: 'host-variant',
+          expectedChain: [{ id: 'host/replacement', variant: 'host-variant' }],
+        },
+      ] as const;
+
+      for (const current of cases) {
+        const root = mkdtempSync(join(tmpdir(), 'marketplace-model-variant-'));
+        try {
+          const store = new MarketplaceStore({
+            rootDir: root,
+            pluginVersion: '3.0.0-beta.3',
+          });
+          store.install(
+            bundle({
+              id: 'community/model-variant-chain',
+              agentName: 'model-variant-chain',
+              skills: [],
+              mcps: [],
+              tools: ['read'],
+              model: {
+                source: 'explicit',
+                candidates: [
+                  { id: 'package/primary', variant: 'package-variant' },
+                  { id: 'package/fallback', variant: 'package-fallback' },
+                ],
+              },
+            }),
+          );
+          RuntimeConfig.reset(root);
+          const runtime = RuntimeConfig.init(root, {
+            preset: 'work',
+            presets: {
+              work: {
+                agents: {
+                  'model-variant-chain': current.owner,
+                  ...(current.orchestrator
+                    ? { orchestrator: current.orchestrator }
+                    : {}),
+                },
+                marketplace: { agents: ['community/model-variant-chain'] },
+              },
+            },
+          });
+          if (current.host) {
+            runtime.captureHostConfig({
+              agent: { 'model-variant-chain': current.host },
+            });
+          }
+
+          const registry = buildResolvedAgentRegistry(runtime, {
+            marketplaceStore: store,
+            availableMcpNames: [],
+          });
+          const sdkConfig = registry.sdkConfigs['model-variant-chain'];
+          expect(sdkConfig.model).toBe(current.expectedModel);
+          expect(sdkConfig.variant).toBe(current.expectedVariant);
+          expect(registry.modelArrays['model-variant-chain']).toEqual(
+            current.expectedChain,
+          );
+        } finally {
+          rmSync(root, { recursive: true, force: true });
+        }
+      }
+    },
+    { timeout: 15_000 },
+  );
+
+  test('uses canonical retirement IDs and rejects them before install', () => {
+    expect(RETIRED_MARKETPLACE_PACKAGE_IDS).toHaveLength(3);
+    const store = new MarketplaceStore({
+      rootDir: mkdtempSync(join(tmpdir(), 'marketplace-retired-')),
+    });
+    expect(() =>
+      store.install(bundle({ id: RETIRED_MARKETPLACE_PACKAGE_IDS[0] })),
+    ).toThrow('retired');
+  });
+
+  test('activation reports deterministic collisions', () => {
+    const root = mkdtempSync(join(tmpdir(), 'marketplace-collision-'));
+    try {
+      const store = new MarketplaceStore({
+        rootDir: root,
+        pluginVersion: '3.0.0-beta.3',
+      });
+      store.install(
+        bundle({
+          id: 'community/one',
+          agentName: 'same',
+          skills: [],
+          mcps: [],
+          tools: [],
+        }),
+      );
+      store.install(
+        bundle({
+          id: 'community/two',
+          agentName: 'same',
+          skills: [],
+          mcps: [],
+          tools: [],
+        }),
+      );
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: {},
+            marketplace: { agents: ['community/two', 'community/one'] },
+          },
+        },
+      });
+      const plan = resolveMarketplaceActivation({
+        runtime,
+        store,
+        availableSkillNames: [],
+        availableMcpNames: [],
+      });
+      expect(plan.agents).toHaveLength(1);
+      expect(
+        plan.diagnostics.some((diagnostic) => diagnostic.code === 'collision'),
+      ).toBe(true);
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+
+  test('caps marketplace permissions and MCPs on v1 and v2 hosts', () => {
+    const root = mkdtempSync(join(tmpdir(), 'marketplace-capabilities-'));
+    try {
+      const store = new MarketplaceStore({
+        rootDir: root,
+        pluginVersion: '3.0.0-beta.3',
+      });
+      store.install(
+        bundle({
+          id: 'community/capped',
+          agentName: 'capped',
+          skills: [],
+          mcps: ['context7'],
+          tools: ['read'],
+        }),
+      );
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: {
+              capped: {
+                permission: {
+                  '*': 'allow',
+                  read: 'ask',
+                  'context7_*': 'deny',
+                },
+              },
+            },
+            marketplace: { agents: ['community/capped'] },
+          },
+        },
+      });
+      runtime.captureHostConfig({
+        agent: {
+          capped: {
+            permission: { '*': 'allow', edit: 'allow', 'gh_grep_*': 'allow' },
+            tools: { task: true, acp_run: true, edit: true, read: true },
+          },
+        },
+      });
+      const registry = buildResolvedAgentRegistry(runtime, {
+        marketplaceStore: store,
+        availableMcpNames: ['context7', 'gh_grep'],
+        preflightMcpNames: ['context7'],
+      });
+      const permission = registry.sdkConfigs.capped.permission as Record<
+        string,
+        unknown
+      >;
+      expect(permission['*']).toBe('deny');
+      expect(permission.read).toBe('ask');
+      expect(permission.edit).toBe('deny');
+      expect(permission.task).toBe('deny');
+      expect(permission.acp_run).toBe('deny');
+      expect(permission.lsp).toBe('deny');
+      expect(permission.list).toBe('deny');
+      expect(permission.codesearch).toBe('deny');
+      expect(permission['context7_*']).toBe('deny');
+      expect(permission['gh_grep_*']).toBe('deny');
+      expect(registry.sdkConfigs.capped.tools).toEqual({
+        read: true,
+      });
+
+      const v2Rules = adaptPermissions(permission);
+      expect(
+        v2Rules.some(
+          (rule) => rule.resource === 'read' && rule.effect === 'ask',
+        ),
+      ).toBe(true);
+      expect(
+        v2Rules.some(
+          (rule) => rule.resource === 'codesearch' && rule.effect === 'deny',
+        ),
+      ).toBe(true);
+      expect(
+        v2Rules.some(
+          (rule) => rule.resource === 'context7_*' && rule.effect === 'deny',
+        ),
+      ).toBe(true);
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+
+  test('projects owner and host wildcard deny/ask onto package surfaces', () => {
+    for (const owner of [true, false]) {
+      for (const action of ['deny', 'ask'] as const) {
+        const root = mkdtempSync(join(tmpdir(), 'marketplace-wildcard-'));
+        try {
+          const store = new MarketplaceStore({
+            rootDir: root,
+            pluginVersion: '3.0.0-beta.3',
+          });
+          store.install(
+            bundle({
+              id: 'community/wildcard',
+              agentName: 'wildcard',
+              skills: [],
+              mcps: ['context7'],
+              tools: ['read'],
+            }),
+          );
+          RuntimeConfig.reset(root);
+          const runtime = RuntimeConfig.init(root, {
+            preset: 'work',
+            presets: {
+              work: {
+                agents: owner
+                  ? { wildcard: { permission: { '*': action } } }
+                  : {},
+                marketplace: { agents: ['community/wildcard'] },
+              },
+            },
+          });
+          if (!owner) {
+            runtime.captureHostConfig({
+              agent: { wildcard: { permission: { '*': action } } },
+            });
+          }
+          const permission = buildResolvedAgentRegistry(runtime, {
+            marketplaceStore: store,
+            availableMcpNames: ['context7'],
+            preflightMcpNames: ['context7'],
+          }).sdkConfigs.wildcard.permission as Record<string, unknown>;
+
+          expect(permission.read).toBe(action);
+          expect(permission['context7_*']).toBe(action);
+          expect(permission.edit).toBe('deny');
+        } finally {
+          rmSync(root, { recursive: true, force: true });
+        }
+      }
+    }
+  });
+
+  test('keeps empty and limited skill ceilings default-deny', () => {
+    const cases = [
+      {
+        id: 'community/empty-skills',
+        agentName: 'empty-skills',
+        manifest: { skills: [] },
+        override: { skills: ['*', 'undeclared'] },
+        expectedPermission: {},
+      },
+      {
+        id: 'community/limited-skills',
+        agentName: 'limited-skills',
+        manifest: { skills: ['simplify'] },
+        override: { skills: ['*', 'undeclared'] },
+        expectedPermission: { simplify: 'allow' },
+      },
+      {
+        id: 'community/denied-skills',
+        agentName: 'denied-skills',
+        manifest: { skills: ['simplify'] },
+        override: { permission: { skill: 'deny' } },
+        expectedPermission: 'deny',
+      },
+    ] as const;
+
+    for (const current of cases) {
+      const root = mkdtempSync(join(tmpdir(), 'marketplace-skills-'));
+      try {
+        const store = new MarketplaceStore({
+          rootDir: root,
+          pluginVersion: '3.0.0-beta.3',
+        });
+        store.install(
+          bundle({
+            id: current.id,
+            agentName: current.agentName,
+            skills: current.manifest.skills,
+            mcps: [],
+            tools: ['read'],
+          }),
+        );
+        RuntimeConfig.reset(root);
+        const runtime = RuntimeConfig.init(root, {
+          preset: 'work',
+          presets: {
+            work: {
+              agents: { [current.agentName]: current.override },
+              marketplace: { agents: [current.id] },
+            },
+          },
+        });
+        const registry = buildResolvedAgentRegistry(runtime, {
+          marketplaceStore: store,
+          availableMcpNames: [],
+        });
+        const permission = registry.sdkConfigs[current.agentName]
+          .permission as Record<string, unknown>;
+        expect(permission.skill).toEqual(current.expectedPermission);
+        expect(registry.skillPermissions[current.agentName]).toEqual(
+          current.expectedPermission === 'deny'
+            ? {}
+            : current.expectedPermission,
+        );
+      } finally {
+        rmSync(root, { recursive: true, force: true });
+      }
+    }
+  });
+
+  test('owner skill deny remains deny under host wildcard and nested ask', () => {
+    const root = mkdtempSync(join(tmpdir(), 'marketplace-skill-deny-'));
+    try {
+      const store = new MarketplaceStore({
+        rootDir: root,
+        pluginVersion: '3.0.0-beta.3',
+      });
+      store.install(
+        bundle({
+          id: 'community/skill-deny',
+          agentName: 'skill-deny',
+          skills: ['simplify'],
+          mcps: [],
+          tools: ['read'],
+        }),
+      );
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: {
+              'skill-deny': { permission: { skill: { '*': 'deny' } } },
+            },
+            marketplace: { agents: ['community/skill-deny'] },
+          },
+        },
+      });
+      runtime.captureHostConfig({
+        agent: {
+          'skill-deny': {
+            permission: { '*': 'ask', skill: { '*': 'ask' } },
+          },
+        },
+      });
+      const registry = buildResolvedAgentRegistry(runtime, {
+        marketplaceStore: store,
+        availableMcpNames: [],
+      });
+      expect(registry.sdkConfigs['skill-deny'].permission?.skill).toBe('deny');
+      expect(registry.skillPermissions['skill-deny']).toEqual({});
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+});

+ 12 - 19
src/marketplace/compatibility.test.ts

@@ -8,33 +8,26 @@ import type { MarketplacePackageBundle } from './schemas';
 
 
 const bundle: MarketplacePackageBundle = {
 const bundle: MarketplacePackageBundle = {
   manifest: {
   manifest: {
-    schemaVersion: 1,
+    schemaVersion: 2,
     id: 'community/compatible',
     id: 'community/compatible',
     version: '1.0.0',
     version: '1.0.0',
-    kind: 'profile',
-    displayName: 'Compatible profile',
-    description: 'A compatible profile.',
-    instructions: 'Append these instructions.',
+    displayName: 'Compatible agent',
+    description: 'A compatible agent.',
+    agentName: 'compatible',
+    prompt: 'Append these instructions.',
     author: { name: 'Community' },
     author: { name: 'Community' },
     tags: ['test'],
     tags: ['test'],
     license: 'MIT',
     license: 'MIT',
-    compatibility: {
-      plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-      roleContract: '^1.0.0',
-    },
+    compatibility: { plugin: '>=3.0.0-beta.3 <4.0.0' },
     routing: {
     routing: {
       description: 'Use for compatibility tests.',
       description: 'Use for compatibility tests.',
       keywords: ['test'],
       keywords: ['test'],
-      delegation: { when: 'When testing.', preferredRoles: [] },
-    },
-    requirements: {
-      skills: { required: ['one'], optional: ['two'] },
-      mcps: { required: [], optional: ['three'] },
+      when: 'When testing.',
     },
     },
-    capabilities: { tools: ['read'], permissions: ['filesystem.read'] },
-    targetRole: 'oracle',
-    instructionMode: 'append',
-    overrides: {},
+    skills: ['one'],
+    mcps: [],
+    tools: ['read'],
+    model: { source: 'explicit', candidates: ['provider/model'] },
   },
   },
 };
 };
 
 
@@ -45,7 +38,7 @@ describe('marketplace compatibility', () => {
     expect(satisfiesPluginCompatibility('2.2.18', '^2.0.0')).toBe(true);
     expect(satisfiesPluginCompatibility('2.2.18', '^2.0.0')).toBe(true);
   });
   });
 
 
-  test('rejects incompatible packages before persistence', () => {
+  test('rejects incompatible agents before persistence', () => {
     const service = new MarketplaceService({
     const service = new MarketplaceService({
       rootDir: '/tmp/marketplace-compatibility-test',
       rootDir: '/tmp/marketplace-compatibility-test',
       pluginVersion: '1.0.0',
       pluginVersion: '1.0.0',

+ 1 - 12
src/marketplace/compatibility.ts

@@ -1,14 +1,10 @@
 import { satisfies, valid } from 'semver';
 import { satisfies, valid } from 'semver';
 import { readPluginPackageVersion } from '../utils/package-metadata';
 import { readPluginPackageVersion } from '../utils/package-metadata';
 import { MarketplaceCompatibilityError } from './errors';
 import { MarketplaceCompatibilityError } from './errors';
-import {
-  MARKETPLACE_ROLE_CONTRACT_VERSION,
-  type MarketplacePackageManifest,
-} from './schemas';
+import type { MarketplacePackageManifest } from './schemas';
 
 
 export interface MarketplaceCompatibilityOptions {
 export interface MarketplaceCompatibilityOptions {
   pluginVersion?: string;
   pluginVersion?: string;
-  roleContractVersion?: string;
 }
 }
 
 
 export function satisfiesPluginCompatibility(
 export function satisfiesPluginCompatibility(
@@ -28,8 +24,6 @@ export function validateMarketplaceCompatibility(
       'Cannot determine the installed plugin version for marketplace compatibility',
       'Cannot determine the installed plugin version for marketplace compatibility',
     );
     );
   }
   }
-  const roleContractVersion =
-    options.roleContractVersion ?? MARKETPLACE_ROLE_CONTRACT_VERSION;
   if (
   if (
     !satisfiesPluginCompatibility(pluginVersion, manifest.compatibility.plugin)
     !satisfiesPluginCompatibility(pluginVersion, manifest.compatibility.plugin)
   ) {
   ) {
@@ -37,9 +31,4 @@ export function validateMarketplaceCompatibility(
       `${manifest.id}@${manifest.version} requires plugin ${manifest.compatibility.plugin}; current plugin is ${pluginVersion}`,
       `${manifest.id}@${manifest.version} requires plugin ${manifest.compatibility.plugin}; current plugin is ${pluginVersion}`,
     );
     );
   }
   }
-  if (!satisfies(roleContractVersion, manifest.compatibility.roleContract)) {
-    throw new MarketplaceCompatibilityError(
-      `${manifest.id}@${manifest.version} requires role contract ${manifest.compatibility.roleContract}; current contract is ${roleContractVersion}`,
-    );
-  }
 }
 }

+ 9 - 14
src/marketplace/config-references.test.ts

@@ -13,33 +13,28 @@ const previousConfigHome = process.env.XDG_CONFIG_HOME;
 
 
 const bundle: MarketplacePackageBundle = {
 const bundle: MarketplacePackageBundle = {
   manifest: {
   manifest: {
-    schemaVersion: 1,
+    schemaVersion: 2,
     id: 'community/referenced',
     id: 'community/referenced',
     version: '1.0.0',
     version: '1.0.0',
-    kind: 'agent',
     displayName: 'Referenced agent',
     displayName: 'Referenced agent',
     description: 'An agent used by a preset.',
     description: 'An agent used by a preset.',
-    instructions: 'Follow the role instructions.',
+    agentName: 'referenced',
+    prompt: 'Follow the role instructions.',
     author: { name: 'Community' },
     author: { name: 'Community' },
     tags: ['test'],
     tags: ['test'],
     license: 'MIT',
     license: 'MIT',
     compatibility: {
     compatibility: {
-      plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-      roleContract: '^1.0.0',
+      plugin: '>=3.0.0-beta.3 <4.0.0',
     },
     },
     routing: {
     routing: {
       description: 'Route referenced work here.',
       description: 'Route referenced work here.',
       keywords: ['reference'],
       keywords: ['reference'],
-      delegation: { when: 'When referenced.', preferredRoles: [] },
-    },
-    requirements: {
-      skills: { required: [], optional: [] },
-      mcps: { required: [], optional: [] },
+      when: 'When referenced.',
     },
     },
-    capabilities: { tools: [], permissions: [] },
-    baseRole: 'explorer',
-    agentName: 'referenced',
-    overrides: {},
+    skills: [],
+    mcps: [],
+    tools: [],
+    model: { source: 'explicit', candidates: ['provider/model'] },
   },
   },
 };
 };
 
 

+ 2 - 9
src/marketplace/config-references.ts

@@ -4,7 +4,7 @@ export interface MarketplaceConfigReference {
   packageId: string;
   packageId: string;
   configPath: string;
   configPath: string;
   presetName: string;
   presetName: string;
-  target: 'agent' | 'profile';
+  target: 'agent';
 }
 }
 
 
 interface UnknownRecord {
 interface UnknownRecord {
@@ -20,7 +20,7 @@ function addReference(
   value: unknown,
   value: unknown,
   configPath: string,
   configPath: string,
   presetName: string,
   presetName: string,
-  target: 'agent' | 'profile',
+  target: 'agent',
 ): void {
 ): void {
   if (typeof value !== 'string' || value.trim().length === 0) return;
   if (typeof value !== 'string' || value.trim().length === 0) return;
   references.push({
   references.push({
@@ -50,13 +50,6 @@ function readReferencesFromMergedConfig(
       addReference(references, packageId, configPath, presetName, 'agent');
       addReference(references, packageId, configPath, presetName, 'agent');
     }
     }
   }
   }
-  if (isRecord(marketplace.profiles)) {
-    for (const packageId of Object.values(marketplace.profiles)) {
-      if (packageId !== null) {
-        addReference(references, packageId, configPath, presetName, 'profile');
-      }
-    }
-  }
   return references;
   return references;
 }
 }
 
 

+ 390 - 396
src/marketplace/lease.test.ts

@@ -1,548 +1,542 @@
-import { describe, expect, test } from 'bun:test';
+import { describe, expect, spyOn, test } from 'bun:test';
+import * as fs from 'node:fs';
 import {
 import {
+  closeSync,
   existsSync,
   existsSync,
+  lstatSync,
   mkdirSync,
   mkdirSync,
   mkdtempSync,
   mkdtempSync,
+  openSync,
   readdirSync,
   readdirSync,
   readFileSync,
   readFileSync,
   rmSync,
   rmSync,
+  symlinkSync,
   utimesSync,
   utimesSync,
   writeFileSync,
   writeFileSync,
 } from 'node:fs';
 } from 'node:fs';
 import { tmpdir } from 'node:os';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
 import { join } from 'node:path';
-import { MarketplaceBusyError, MarketplaceLockOwnershipError } from './errors';
 import {
 import {
-  acquireMarketplaceLease,
-  acquireMarketplaceLeaseForTests,
-  generationPath,
-  heartbeatPath,
-  isHeartbeatStale,
-  type LeaseIdentity,
-  listValidGenerations,
-  publishGeneration,
-  readGenerationState,
-  readLeaseState,
-} from './lease';
-import { getMarketplacePaths } from './paths';
+  MarketplaceBusyError,
+  MarketplaceLockfileError,
+  MarketplaceLockOwnershipError,
+} from './errors';
+import { acquireMarketplaceLease, type MarketplaceLockOptions } from './lease';
+import { getMarketplacePaths, type MarketplacePaths } from './paths';
 
 
-const SHORT_LOCK = {
+const LOCK: MarketplaceLockOptions = {
   staleMs: 80,
   staleMs: 80,
-  heartbeatMs: 20,
   timeoutMs: 400,
   timeoutMs: 400,
   retryMs: 10,
   retryMs: 10,
-} as const;
+};
+
+const UUIDS = {
+  first: '00000000-0000-4000-8000-000000000001',
+  second: '00000000-0000-4000-8000-000000000002',
+  third: '00000000-0000-4000-8000-000000000003',
+};
 
 
 function tempRoot(): string {
 function tempRoot(): string {
   return mkdtempSync(join(tmpdir(), 'marketplace-lease-'));
   return mkdtempSync(join(tmpdir(), 'marketplace-lease-'));
 }
 }
 
 
-function expireHeartbeat(directory: string, generation: string): void {
-  const old = new Date(Date.now() - 2_000);
-  const file = heartbeatPath(directory, generation);
-  utimesSync(generationPath(directory, generation), old, old);
-  utimesSync(file, old, old);
+function entryPath(
+  lockDir: string,
+  pid: number,
+  uuid: string,
+  kind: 'candidate' | 'lease',
+): string {
+  return join(lockDir, `${pid}.${uuid}.${kind}`);
 }
 }
 
 
-function writeIncompleteGeneration(
-  directory: string,
-  generation: string,
-): void {
-  const target = generationPath(directory, generation);
-  mkdirSync(target, { recursive: true });
-  writeFileSync(
-    join(target, 'meta.json'),
-    `${JSON.stringify({ generation })}\n`,
-  );
+function createEntry(
+  lockDir: string,
+  pid: number,
+  uuid: string,
+  kind: 'candidate' | 'lease',
+): string {
+  mkdirSync(lockDir, { recursive: true });
+  const filePath = entryPath(lockDir, pid, uuid, kind);
+  const fd = openSync(filePath, 'wx', 0o600);
+  closeSync(fd);
+  return filePath;
+}
+
+function age(filePath: string, milliseconds = 2_000): void {
+  const old = new Date(Date.now() - milliseconds);
+  utimesSync(filePath, old, old);
 }
 }
 
 
-function creatingDirs(directory: string): string[] {
+function lockEntries(lockDir: string): string[] {
   try {
   try {
-    return readdirSync(directory).filter((name) =>
-      name.startsWith('.creating.'),
-    );
+    return readdirSync(lockDir);
   } catch {
   } catch {
     return [];
     return [];
   }
   }
 }
 }
 
 
-async function waitForFile(filePath: string, timeoutMs = 3_000): Promise<void> {
-  const started = Date.now();
-  while (!existsSync(filePath)) {
-    if (Date.now() - started > timeoutMs) {
-      throw new Error(`Timed out waiting for ${filePath}`);
-    }
-    await Bun.sleep(10);
-  }
-}
-
-function spawnLeaseWorker(
+function spawnWorker(
+  paths: MarketplacePaths,
   body: string,
   body: string,
-  payload: Record<string, unknown>,
-  env: Record<string, string | undefined> = {},
+  extra: Record<string, string> = {},
 ): Bun.Subprocess {
 ): Bun.Subprocess {
   return Bun.spawn(
   return Bun.spawn(
     [
     [
       'bun',
       'bun',
       '-e',
       '-e',
-      `import { existsSync, writeFileSync } from 'node:fs';
-import { acquireMarketplaceLease } from './src/marketplace/lease.ts';
+      `import { acquireMarketplaceLease } from './src/marketplace/lease.ts';
 ${body}`,
 ${body}`,
-      JSON.stringify(payload),
+      JSON.stringify({ paths, ...extra }),
     ],
     ],
-    {
-      stdout: 'pipe',
-      stderr: 'pipe',
-      env: { ...process.env, ...env },
-    },
+    { stdout: 'pipe', stderr: 'pipe' },
   );
   );
 }
 }
 
 
-async function workerOutput(
-  worker: Bun.Subprocess,
-): Promise<{ code: number; stdout: string; stderr: string }> {
-  const [stdout, stderr, code] = await Promise.all([
-    new Response(worker.stdout).text(),
+async function workerResult(worker: Bun.Subprocess): Promise<{
+  code: number;
+  stderr: string;
+}> {
+  const [stderr, code] = await Promise.all([
     new Response(worker.stderr).text(),
     new Response(worker.stderr).text(),
     worker.exited,
     worker.exited,
   ]);
   ]);
-  return { code, stdout, stderr };
+  return { code, stderr };
 }
 }
 
 
-describe('marketplace lease generations', () => {
-  test('stale cleanup does not claim a replacement generation', () => {
+function withKillError<T>(pid: number, code: string, operation: () => T): T {
+  const originalKill = process.kill;
+  const kill = spyOn(process, 'kill').mockImplementation(((target, signal) => {
+    if (target === pid && signal === 0) {
+      throw Object.assign(new Error(`injected ${code}`), { code });
+    }
+    return originalKill(target, signal);
+  }) as typeof process.kill);
+  try {
+    return operation();
+  } finally {
+    kill.mockRestore();
+  }
+}
+
+describe('marketplace synchronous lease lifecycle', () => {
+  test('acquires, commits, and releases one empty regular lease file', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
     try {
     try {
-      mkdirSync(paths.rootDir, { recursive: true });
-      const replacement: LeaseIdentity = {
-        generation: 'replacement-generation',
-        token: 'replacement-token',
-      };
-      expect(publishGeneration(paths.lockDir, replacement)).toBe(true);
-      writeIncompleteGeneration(paths.lockDir, 'stale-generation');
-      expect(() =>
-        acquireMarketplaceLease(paths, {
-          staleMs: 2_000,
-          heartbeatMs: 100,
-          timeoutMs: 120,
-          retryMs: 10,
-        }),
-      ).toThrow(MarketplaceBusyError);
-      expect(readLeaseState(paths.lockDir)?.metadata).toEqual({
-        ...replacement,
-        pid: process.pid,
-      });
-      expect(
-        existsSync(generationPath(paths.lockDir, 'stale-generation')),
-      ).toBe(true);
+      const lease = acquireMarketplaceLease(paths, LOCK);
+      const entries = lockEntries(paths.lockDir);
+      expect(entries).toHaveLength(1);
+      expect(entries[0]).toMatch(/^[1-9][0-9]*\.[0-9a-f-]+\.lease$/);
+      const stat = lstatSync(join(paths.lockDir, entries[0]));
+      expect(stat.isFile()).toBe(true);
+      expect(stat.size).toBe(0);
+      expect(() => lease.assertCurrent()).not.toThrow();
+      expect(lease.commit(() => 'done')).toBe('done');
+      lease.release();
+      expect(lockEntries(paths.lockDir)).toEqual([]);
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('missing metadata is not ownership and is never deleted', () => {
+  test('operation failure preserves ownership for release', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
     try {
     try {
-      writeIncompleteGeneration(paths.lockDir, 'partial-generation');
-      const owner = acquireMarketplaceLease(paths, SHORT_LOCK);
-      expect(
-        existsSync(
-          join(
-            generationPath(paths.lockDir, 'partial-generation'),
-            'meta.json',
-          ),
-        ),
-      ).toBe(true);
-      expect(
-        JSON.parse(
-          readFileSync(
-            join(
-              generationPath(paths.lockDir, 'partial-generation'),
-              'meta.json',
-            ),
-            'utf8',
-          ),
-        ),
-      ).toEqual({ generation: 'partial-generation' });
-      expect(owner.generation).not.toBe('partial-generation');
-      owner.release();
+      const lease = acquireMarketplaceLease(paths, LOCK);
+      expect(() =>
+        lease.commit(() => {
+          throw new Error('operation failed');
+        }),
+      ).toThrow('operation failed');
+      expect(() => lease.assertCurrent()).not.toThrow();
+      expect(() => lease.release()).not.toThrow();
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('heartbeat equal to the stale interval is still fresh', () => {
+  test('released leases cannot be used again', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
     try {
     try {
-      const identity: LeaseIdentity = {
-        generation: 'boundary-generation',
-        token: 'boundary-token',
-      };
-      expect(publishGeneration(paths.lockDir, identity)).toBe(true);
-      const staleMs = 200;
-      const state = readGenerationState(
-        generationPath(paths.lockDir, identity.generation),
+      const lease = acquireMarketplaceLease(paths, LOCK);
+      lease.release();
+      expect(() => lease.assertCurrent()).toThrow(
+        MarketplaceLockOwnershipError,
       );
       );
-      if (!state) throw new Error('expected published generation state');
-      expect(
-        isHeartbeatStale(state, staleMs, state.heartbeatMtimeMs + staleMs),
-      ).toBe(false);
+      expect(() => lease.commit(() => undefined)).toThrow(
+        MarketplaceLockOwnershipError,
+      );
+      expect(() => lease.release()).toThrow(MarketplaceLockOwnershipError);
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('heartbeat older than the stale interval is stale', () => {
+  test('a live stale owner blocks acquisition indefinitely until release', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
     try {
     try {
-      const identity: LeaseIdentity = {
-        generation: 'stale-boundary-generation',
-        token: 'stale-boundary-token',
-      };
-      expect(publishGeneration(paths.lockDir, identity)).toBe(true);
-      const staleMs = 200;
-      const state = readGenerationState(
-        generationPath(paths.lockDir, identity.generation),
-      );
-      if (!state) throw new Error('expected published generation state');
-      expect(
-        isHeartbeatStale(state, staleMs, state.heartbeatMtimeMs + staleMs + 1),
-      ).toBe(true);
+      const owner = acquireMarketplaceLease(paths, LOCK);
+      const leasePath = join(paths.lockDir, lockEntries(paths.lockDir)[0]);
+      age(leasePath);
+      expect(() =>
+        acquireMarketplaceLease(paths, { ...LOCK, timeoutMs: 120 }),
+      ).toThrow(MarketplaceBusyError);
+      expect(() => owner.assertCurrent()).not.toThrow();
+      owner.release();
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('synchronous commit remains owned after the stale interval', () => {
+  test('synchronous blocking work remains owned beyond the stale interval', async () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
-    let nowMs = Date.now();
+    const resultPath = join(root, 'challenger-result');
+    const startedPath = join(root, 'challenger-started');
+    let challenger: Bun.Subprocess | undefined;
     try {
     try {
-      const owner = acquireMarketplaceLeaseForTests(
+      const owner = acquireMarketplaceLease(paths, LOCK);
+      challenger = spawnWorker(
         paths,
         paths,
-        {
-          staleMs: 40,
-          heartbeatMs: 10,
-          timeoutMs: 500,
-          retryMs: 5,
-        },
-        () => nowMs,
-      );
-      const acquired = readGenerationState(
-        generationPath(paths.lockDir, owner.generation),
-      );
-      if (!acquired) throw new Error('expected owned generation');
-      nowMs = acquired.heartbeatMtimeMs;
-      const result = owner.commit(() => {
-        const during = readGenerationState(
-          generationPath(paths.lockDir, owner.generation),
-        );
-        if (!during) {
-          throw new Error('expected owned generation during commit');
-        }
-        nowMs = during.heartbeatMtimeMs + 80;
-        return 'published';
-      });
-      expect(result).toBe('published');
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        owner.generation,
+        `const { paths, resultPath, startedPath } = JSON.parse(process.argv[1]);
+await import('node:fs').then(({ writeFileSync }) => writeFileSync(startedPath, 'started'));
+try {
+  const lease = acquireMarketplaceLease(paths, { staleMs: 80, timeoutMs: 120, retryMs: 10 });
+  lease.release();
+  await import('node:fs').then(({ writeFileSync }) => writeFileSync(resultPath, 'acquired'));
+} catch (error) {
+  await import('node:fs').then(({ writeFileSync }) => writeFileSync(resultPath, error instanceof Error ? error.constructor.name : 'unknown'));
+}`,
+        { resultPath, startedPath },
       );
       );
+      for (
+        let attempt = 0;
+        attempt < 200 && !existsSync(startedPath);
+        attempt++
+      ) {
+        await Bun.sleep(5);
+      }
+      expect(existsSync(startedPath)).toBe(true);
+      Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
+      expect(() => owner.assertCurrent()).not.toThrow();
+      owner.commit(() => undefined);
       owner.release();
       owner.release();
+      await challenger.exited;
+      expect(readFileSync(resultPath, 'utf8')).toBe('MarketplaceBusyError');
     } finally {
     } finally {
+      challenger?.kill();
+      if (challenger) await challenger.exited.catch(() => -1);
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('expired owner cannot commit before a replacement exists', () => {
+  test('uses the default process liveness mapping for stale leases', () => {
+    for (const [code, reclaimable] of [
+      ['ESRCH', true],
+      ['EPERM', false],
+      ['EUNKNOWN', false],
+    ] as const) {
+      const root = tempRoot();
+      const paths = getMarketplacePaths(root);
+      try {
+        const stale = createEntry(paths.lockDir, 4242, UUIDS.first, 'lease');
+        age(stale);
+        if (reclaimable) {
+          const owner = withKillError(4242, code, () =>
+            acquireMarketplaceLease(paths, LOCK),
+          );
+          expect(existsSync(stale)).toBe(false);
+          owner.release();
+        } else {
+          expect(() =>
+            withKillError(4242, code, () =>
+              acquireMarketplaceLease(paths, { ...LOCK, timeoutMs: 120 }),
+            ),
+          ).toThrow(MarketplaceBusyError);
+          expect(existsSync(stale)).toBe(true);
+        }
+      } finally {
+        rmSync(root, { recursive: true, force: true });
+      }
+    }
+  });
+
+  test('aged candidates are reclaimed regardless of PID', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
     try {
     try {
-      const owner = acquireMarketplaceLease(paths, SHORT_LOCK);
-      expireHeartbeat(paths.lockDir, owner.generation);
-      let published = false;
-      expect(() =>
-        owner.commit(() => {
-          published = true;
-        }),
-      ).toThrow(MarketplaceLockOwnershipError);
-      expect(published).toBe(false);
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        owner.generation,
+      const candidate = createEntry(
+        paths.lockDir,
+        process.pid,
+        UUIDS.first,
+        'candidate',
       );
       );
-      expect(() => owner.release()).not.toThrow();
+      age(candidate);
+      const owner = acquireMarketplaceLease(paths, LOCK);
+      expect(existsSync(candidate)).toBe(false);
+      owner.release();
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
-});
 
 
-describe('marketplace lease subprocess barriers', () => {
-  test('active breaker blocks main lock acquire', async () => {
+  test('candidate election leaves exactly one lease under concurrent acquisition', async () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
-    const readyPath = join(root, 'ready');
-    const holdingPath = join(root, 'holding');
-    const goPath = join(root, 'go');
-    const worker = spawnLeaseWorker(
-      `const { paths, readyPath, holdingPath, goPath, lock } = JSON.parse(process.argv[1]);
-const lease = acquireMarketplaceLease(paths, lock);
-writeFileSync(readyPath, lease.generation);
-lease.commit(() => {
-  writeFileSync(holdingPath, 'holding');
-  while (!existsSync(goPath)) {
-    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
-  }
+    const barrierDir = join(root, 'barrier');
+    const goPath = join(barrierDir, 'go');
+    mkdirSync(barrierDir);
+    const workers = Array.from({ length: 2 }, () =>
+      spawnWorker(
+        paths,
+        `const { paths, barrierDir, goPath } = JSON.parse(process.argv[1]);
+const fs = await import('node:fs');
+const lease = acquireMarketplaceLease(paths, { staleMs: 500, timeoutMs: 3000, retryMs: 5 }, {
+  afterCandidateCreated(candidatePath) {
+    const readyPath = barrierDir + '/' + candidatePath.split('/').pop() + '.ready';
+    fs.writeFileSync(readyPath, 'ready');
+    while (!fs.existsSync(goPath)) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
+  },
 });
 });
 lease.release();`,
 lease.release();`,
-      { paths, readyPath, holdingPath, goPath, lock: SHORT_LOCK },
+        { barrierDir, goPath },
+      ),
     );
     );
     try {
     try {
-      await waitForFile(holdingPath);
-      const generation = readFileSync(readyPath, 'utf8');
-      expireHeartbeat(paths.lockDir, generation);
-      expect(() =>
-        acquireMarketplaceLease(paths, { ...SHORT_LOCK, timeoutMs: 120 }),
-      ).toThrow(MarketplaceBusyError);
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        generation,
-      );
-      expect(listValidGenerations(paths.breakerDir).length).toBeGreaterThan(0);
+      for (
+        let attempt = 0;
+        attempt < 300 && readdirSync(barrierDir).length < 2;
+        attempt++
+      ) {
+        await Bun.sleep(10);
+      }
+      expect(readdirSync(barrierDir)).toHaveLength(2);
       writeFileSync(goPath, 'go');
       writeFileSync(goPath, 'go');
-      const result = await workerOutput(worker);
-      expect(result.code).toBe(0);
+      const results = await Promise.all(workers.map(workerResult));
+      expect(results.every((result) => result.code === 0)).toBe(true);
+      expect(results.map((result) => result.stderr)).toEqual(['', '']);
+      expect(lockEntries(paths.lockDir)).toEqual([]);
     } finally {
     } finally {
-      worker.kill();
-      await worker.exited.catch(() => undefined);
+      for (const worker of workers) worker.kill();
+      await Promise.all(workers.map((worker) => worker.exited.catch(() => -1)));
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('kills a worker during private generation initialization', async () => {
+  test('multiple reclaimers exclude one another', async () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
-    const barrierDir = join(root, 'barriers');
-    mkdirSync(barrierDir, { recursive: true });
-    writeFileSync(join(barrierDir, 'prepare-generation.wait'), 'wait');
-    const worker = spawnLeaseWorker(
-      `const { paths, lock } = JSON.parse(process.argv[1]);
-acquireMarketplaceLease(paths, lock);`,
-      { paths, lock: SHORT_LOCK },
-      { MARKETPLACE_LEASE_BARRIER_DIR: barrierDir },
+    const barrierDir = join(root, 'reclaimer-barrier');
+    const goPath = join(barrierDir, 'go');
+    const active = join(root, 'active');
+    mkdirSync(barrierDir);
+    const stale = createEntry(paths.lockDir, 4242, UUIDS.first, 'lease');
+    age(stale);
+    const workers = Array.from({ length: 4 }, () =>
+      spawnWorker(
+        paths,
+        `const { paths, barrierDir, goPath } = JSON.parse(process.argv[1]);
+const fs = await import('node:fs');
+const lease = acquireMarketplaceLease(paths, { staleMs: 500, timeoutMs: 5000, retryMs: 5 }, {
+  afterCandidateCreated(candidatePath) {
+    const readyPath = barrierDir + '/' + candidatePath.split('/').pop() + '.ready';
+    fs.writeFileSync(readyPath, 'ready');
+    while (!fs.existsSync(goPath)) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
+  },
+});
+const fd = fs.openSync(${JSON.stringify(active)}, 'wx');
+fs.closeSync(fd);
+await new Promise((resolve) => setTimeout(resolve, 25));
+fs.unlinkSync(${JSON.stringify(active)});
+lease.release();`,
+        { barrierDir, goPath },
+      ),
     );
     );
     try {
     try {
-      await waitForFile(join(barrierDir, 'prepare-generation.reached'));
-      expect(creatingDirs(paths.breakerDir).length).toBeGreaterThan(0);
-      worker.kill('SIGKILL');
-      await worker.exited;
-      rmSync(join(barrierDir, 'prepare-generation.wait'), { force: true });
-      const owner = acquireMarketplaceLease(paths, SHORT_LOCK);
-      expect(creatingDirs(paths.breakerDir)).toEqual([]);
-      expect(creatingDirs(paths.lockDir)).toEqual([]);
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        owner.generation,
-      );
-      owner.release();
+      for (
+        let attempt = 0;
+        attempt < 300 && readdirSync(barrierDir).length < 4;
+        attempt++
+      ) {
+        await Bun.sleep(10);
+      }
+      expect(readdirSync(barrierDir)).toHaveLength(4);
+      writeFileSync(goPath, 'go');
+      const results = await Promise.all(workers.map(workerResult));
+      expect(results.every((result) => result.code === 0)).toBe(true);
+      expect(results.map((result) => result.stderr)).toEqual(['', '', '', '']);
+      expect(existsSync(active)).toBe(false);
     } finally {
     } finally {
-      worker.kill();
-      await worker.exited.catch(() => undefined);
+      for (const worker of workers) worker.kill();
+      await Promise.all(workers.map((worker) => worker.exited.catch(() => -1)));
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('expired owner commit fails before replacement', async () => {
+  test('replacement fencing unlinks only the exact unique lease path', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
-    const readyPath = join(root, 'ready');
-    const expirePath = join(root, 'expire');
-    const logPath = join(root, 'commit.log');
-    const publishedPath = join(root, 'published');
-    const worker = spawnLeaseWorker(
-      `const { paths, readyPath, expirePath, logPath, publishedPath, lock } = JSON.parse(process.argv[1]);
-const lease = acquireMarketplaceLease(paths, lock);
-writeFileSync(readyPath, lease.generation);
-while (!existsSync(expirePath)) {
-  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
-}
-try {
-  lease.commit(() => writeFileSync(publishedPath, 'published'));
-  writeFileSync(logPath, 'committed');
-} catch (error) {
-  writeFileSync(logPath, error instanceof Error ? error.constructor.name : 'error');
-}`,
-      {
-        paths,
-        readyPath,
-        expirePath,
-        logPath,
-        publishedPath,
-        lock: SHORT_LOCK,
-      },
-    );
     try {
     try {
-      await waitForFile(readyPath);
-      const generation = readFileSync(readyPath, 'utf8');
-      expireHeartbeat(paths.lockDir, generation);
-      writeFileSync(expirePath, 'expired');
-      const result = await workerOutput(worker);
-      expect(result.code).toBe(0);
-      expect(readFileSync(logPath, 'utf8')).toBe(
-        'MarketplaceLockOwnershipError',
-      );
-      expect(existsSync(publishedPath)).toBe(false);
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        generation,
+      const owner = acquireMarketplaceLease(paths, LOCK);
+      const oldPath = join(paths.lockDir, lockEntries(paths.lockDir)[0]);
+      unlinkForTest(oldPath);
+      const replacement = createEntry(
+        paths.lockDir,
+        process.pid,
+        UUIDS.second,
+        'lease',
       );
       );
+      expect(() => owner.release()).toThrow(MarketplaceLockOwnershipError);
+      expect(existsSync(replacement)).toBe(true);
+      unlinkForTest(replacement);
     } finally {
     } finally {
-      worker.kill();
-      await worker.exited.catch(() => undefined);
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('owner release during takeover leaves the replacement', async () => {
+  test('unlink failure leaves ownership retryable while the exact file remains', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
-    const readyPath = join(root, 'ready');
-    const takeoverPath = join(root, 'takeover');
-    const logPath = join(root, 'release.log');
-    const worker = spawnLeaseWorker(
-      `const { paths, readyPath, takeoverPath, logPath, lock } = JSON.parse(process.argv[1]);
-const lease = acquireMarketplaceLease(paths, lock);
-writeFileSync(readyPath, JSON.stringify({ generation: lease.generation, token: lease.token }));
-while (!existsSync(takeoverPath)) {
-  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
-}
-try {
-  lease.release();
-  writeFileSync(logPath, 'released');
-} catch (error) {
-  writeFileSync(logPath, error instanceof Error ? error.constructor.name : 'error');
-}`,
-      { paths, readyPath, takeoverPath, logPath, lock: SHORT_LOCK },
-    );
     try {
     try {
-      await waitForFile(readyPath);
-      const owner = JSON.parse(
-        readFileSync(readyPath, 'utf8'),
-      ) as LeaseIdentity;
-      expireHeartbeat(paths.lockDir, owner.generation);
-      const replacement = acquireMarketplaceLease(paths, SHORT_LOCK);
-      expect(replacement.generation).not.toBe(owner.generation);
-      writeFileSync(takeoverPath, 'taken');
-      const result = await workerOutput(worker);
-      expect(result.code).toBe(0);
-      expect(readFileSync(logPath, 'utf8')).toBe(
-        'MarketplaceLockOwnershipError',
-      );
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        replacement.generation,
+      const owner = acquireMarketplaceLease(paths, LOCK);
+      const leasePath = join(paths.lockDir, lockEntries(paths.lockDir)[0]);
+      const originalUnlink = fs.unlinkSync;
+      let failed = false;
+      const unlink = spyOn(fs, 'unlinkSync').mockImplementation((target) => {
+        if (!failed && target === leasePath) {
+          failed = true;
+          const error = new Error('injected unlink failure') as Error & {
+            code: string;
+          };
+          error.code = 'EACCES';
+          throw error;
+        }
+        return originalUnlink(target);
+      });
+      try {
+        expect(() => owner.release()).toThrow('injected unlink failure');
+        expect(existsSync(leasePath)).toBe(true);
+        expect(() => owner.assertCurrent()).not.toThrow();
+      } finally {
+        unlink.mockRestore();
+      }
+      owner.release();
+      expect(existsSync(leasePath)).toBe(false);
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+
+  test('malformed, unknown, directory, and symlink state fails closed', () => {
+    const cases = [
+      (lockDir: string) => writeFileSync(join(lockDir, 'unknown'), ''),
+      (lockDir: string) => mkdirSync(join(lockDir, `1.${UUIDS.first}.lease`)),
+      (lockDir: string) => {
+        const target = createEntry(lockDir, process.pid, UUIDS.second, 'lease');
+        rmSync(target);
+        symlinkSync('/tmp', target);
+      },
+    ];
+    for (const setup of cases) {
+      const root = tempRoot();
+      const paths = getMarketplacePaths(root);
+      try {
+        mkdirSync(paths.lockDir, { recursive: true });
+        setup(paths.lockDir);
+        expect(() => acquireMarketplaceLease(paths, LOCK)).toThrow(
+          MarketplaceLockfileError,
+        );
+      } finally {
+        rmSync(root, { recursive: true, force: true });
+      }
+    }
+  });
+
+  test('restarts a directory snapshot when a listed entry disappears', () => {
+    const root = tempRoot();
+    const paths = getMarketplacePaths(root);
+    let injected = false;
+    try {
+      acquireMarketplaceLease(paths, LOCK);
+      const originalLstat = fs.lstatSync;
+      const leasePath = join(paths.lockDir, lockEntries(paths.lockDir)[0]);
+      const replacementPath = entryPath(
+        paths.lockDir,
+        process.pid,
+        UUIDS.third,
+        'lease',
       );
       );
-      expect(() => replacement.assertCurrent()).not.toThrow();
-      replacement.release();
+      const lstat = spyOn(fs, 'lstatSync').mockImplementation((target) => {
+        if (!injected && target === leasePath) {
+          injected = true;
+          fs.unlinkSync(leasePath);
+          createEntry(paths.lockDir, process.pid, UUIDS.third, 'lease');
+          throw Object.assign(new Error('entry disappeared'), {
+            code: 'ENOENT',
+          });
+        }
+        return originalLstat(target);
+      });
+      try {
+        expect(() =>
+          acquireMarketplaceLease(paths, {
+            ...LOCK,
+            timeoutMs: 80,
+          }),
+        ).toThrow(MarketplaceBusyError);
+        expect(injected).toBe(true);
+        expect(existsSync(replacementPath)).toBe(true);
+      } finally {
+        lstat.mockRestore();
+      }
     } finally {
     } finally {
-      worker.kill();
-      await worker.exited.catch(() => undefined);
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('three parties cannot overlap critical sections or displace a generation', async () => {
+  test('propagates non-ENOENT snapshot errors fail closed', () => {
     const root = tempRoot();
     const root = tempRoot();
     const paths = getMarketplacePaths(root);
     const paths = getMarketplacePaths(root);
-    const barrierDir = join(root, 'barriers');
-    mkdirSync(barrierDir, { recursive: true });
-    writeFileSync(join(barrierDir, 'breaker-critical.wait'), 'wait');
-    const ownerReady = join(root, 'owner-ready');
-    const ownerGo = join(root, 'owner-go');
-    const staleReady = join(root, 'stale-ready');
-    const owner = spawnLeaseWorker(
-      `const { paths, ownerReady, ownerGo, lock } = JSON.parse(process.argv[1]);
-const lease = acquireMarketplaceLease(paths, lock);
-writeFileSync(ownerReady, lease.generation);
-while (!existsSync(ownerGo)) {
-  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
-}
-lease.release();`,
-      {
-        paths,
-        ownerReady,
-        ownerGo,
-        lock: {
-          staleMs: 2_000,
-          heartbeatMs: 100,
-          timeoutMs: 3_000,
-          retryMs: 20,
-        },
-      },
-    );
-    let staleClaim: Bun.Subprocess | undefined;
     try {
     try {
-      await waitForFile(ownerReady);
-    } catch (error) {
-      owner.kill();
-      await owner.exited.catch(() => undefined);
-      throw error;
+      const entry = createEntry(paths.lockDir, 4242, UUIDS.first, 'lease');
+      const originalLstat = fs.lstatSync;
+      const lstat = spyOn(fs, 'lstatSync').mockImplementation((target) => {
+        if (target === entry) {
+          throw Object.assign(new Error('I/O failure'), { code: 'EIO' });
+        }
+        return originalLstat(target);
+      });
+      try {
+        expect(() => acquireMarketplaceLease(paths, LOCK)).toThrow(
+          MarketplaceLockfileError,
+        );
+      } finally {
+        lstat.mockRestore();
+      }
+    } finally {
+      rmSync(root, { recursive: true, force: true });
     }
     }
-    staleClaim = spawnLeaseWorker(
-      `const { paths, staleReady, lock } = JSON.parse(process.argv[1]);
-writeFileSync(staleReady, 'started');
-try {
-  acquireMarketplaceLease(paths, lock);
-  writeFileSync(staleReady + '.won', 'won');
-} catch (error) {
-  writeFileSync(staleReady + '.busy', error instanceof Error ? error.constructor.name : 'error');
-}`,
-      {
-        paths,
-        staleReady,
-        lock: {
-          staleMs: 2_000,
-          heartbeatMs: 100,
-          timeoutMs: 2_000,
-          retryMs: 20,
-        },
-      },
-      { MARKETPLACE_LEASE_BARRIER_DIR: barrierDir },
-    );
+  });
+
+  test('times out without mutating a live owner', () => {
+    const root = tempRoot();
+    const paths = getMarketplacePaths(root);
     try {
     try {
-      const ownerGeneration = readFileSync(ownerReady, 'utf8');
-      await waitForFile(staleReady);
-      await waitForFile(join(barrierDir, 'breaker-critical.reached'));
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        ownerGeneration,
-      );
+      const owner = acquireMarketplaceLease(paths, LOCK);
       expect(() =>
       expect(() =>
-        acquireMarketplaceLease(paths, {
-          staleMs: 2_000,
-          heartbeatMs: 100,
-          timeoutMs: 120,
-          retryMs: 10,
-        }),
+        acquireMarketplaceLease(paths, { ...LOCK, timeoutMs: 60 }),
       ).toThrow(MarketplaceBusyError);
       ).toThrow(MarketplaceBusyError);
-      expect(readLeaseState(paths.lockDir)?.metadata.generation).toBe(
-        ownerGeneration,
-      );
-      expect(existsSync(`${staleReady}.won`)).toBe(false);
-      writeFileSync(join(barrierDir, 'breaker-critical.go'), 'go');
-      writeFileSync(ownerGo, 'go');
-      const [ownerResult, staleResult] = await Promise.all([
-        workerOutput(owner),
-        workerOutput(staleClaim),
-      ]);
-      expect(ownerResult.code).toBe(0);
-      expect(staleResult.code).toBe(0);
+      expect(lockEntries(paths.lockDir)).toHaveLength(1);
+      owner.release();
     } finally {
     } finally {
-      owner.kill();
-      staleClaim?.kill();
-      await Promise.all([
-        owner.exited.catch(() => undefined),
-        staleClaim?.exited.catch(() => undefined),
-      ]);
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 });
 });
+
+function unlinkForTest(filePath: string): void {
+  fs.unlinkSync(filePath);
+}

+ 234 - 492
src/marketplace/lease.ts

@@ -12,50 +12,46 @@ import type { MarketplacePaths } from './paths';
 export interface MarketplaceLockOptions {
 export interface MarketplaceLockOptions {
   staleMs: number;
   staleMs: number;
   timeoutMs: number;
   timeoutMs: number;
-  heartbeatMs: number;
   retryMs: number;
   retryMs: number;
 }
 }
 
 
 export interface MarketplaceLease {
 export interface MarketplaceLease {
-  generation: string;
-  token: string;
   assertCurrent(): void;
   assertCurrent(): void;
   commit<T>(operation: () => T): T;
   commit<T>(operation: () => T): T;
   release(): void;
   release(): void;
 }
 }
 
 
-export interface LeaseIdentity {
-  generation: string;
-  token: string;
-}
-
-export interface LeaseMetadata extends LeaseIdentity {
-  pid: number;
-}
-
-export interface LeaseState {
-  metadata: LeaseMetadata;
-  heartbeatMtimeMs: number;
-}
-
 const DEFAULT_LOCK_OPTIONS: MarketplaceLockOptions = {
 const DEFAULT_LOCK_OPTIONS: MarketplaceLockOptions = {
   staleMs: 60_000,
   staleMs: 60_000,
   timeoutMs: 300_000,
   timeoutMs: 300_000,
-  heartbeatMs: 20_000,
   retryMs: 20,
   retryMs: 20,
 };
 };
 
 
-function lockNow(now?: () => number): number {
-  return now?.() ?? Date.now();
+const UUID_PATTERN =
+  '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
+const ENTRY_PATTERN = new RegExp(
+  `^([1-9][0-9]*)\\.(${UUID_PATTERN})\\.(candidate|lease)$`,
+);
+const MAX_SCAN_RETRIES = 8;
+
+interface MarketplaceLeaseTestHooks {
+  afterCandidateCreated?: (candidatePath: string) => void;
 }
 }
 
 
-const OWNERSHIP_MESSAGE =
-  'Marketplace lease generation is no longer fresh and current';
+class RestartDirectoryScan extends Error {}
 
 
-const CREATING_PREFIX = '.creating.';
+interface LockEntry {
+  kind: 'candidate' | 'lease';
+  name: string;
+  path: string;
+  pid: number;
+  uuid: string;
+  mtimeMs: number;
+}
 
 
-interface BreakerLease {
-  release(): void;
+interface LockState {
+  candidates: LockEntry[];
+  leases: LockEntry[];
 }
 }
 
 
 function errnoCode(error: unknown): string | undefined {
 function errnoCode(error: unknown): string | undefined {
@@ -64,6 +60,22 @@ function errnoCode(error: unknown): string | undefined {
   return typeof code === 'string' ? code : undefined;
   return typeof code === 'string' ? code : undefined;
 }
 }
 
 
+function sleepSync(milliseconds: number): void {
+  const sleeper = new Int32Array(new SharedArrayBuffer(4));
+  Atomics.wait(sleeper, 0, 0, milliseconds);
+}
+
+function pidIsDead(pid: number): boolean {
+  try {
+    process.kill(pid, 0);
+    return false;
+  } catch (error) {
+    // ESRCH is the only conclusive dead result. EPERM and every unknown
+    // failure are treated as alive so a live owner is never reclaimed.
+    return errnoCode(error) === 'ESRCH';
+  }
+}
+
 export function syncDirectory(directory: string): void {
 export function syncDirectory(directory: string): void {
   try {
   try {
     const fd = fs.openSync(directory, 'r');
     const fd = fs.openSync(directory, 'r');
@@ -95,33 +107,7 @@ export function writeAtomic(filePath: string, content: string): void {
     try {
     try {
       if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
       if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
     } catch {
     } catch {
-      // A failed cleanup cannot make the published file invalid.
-    }
-  }
-}
-
-function sleepSync(milliseconds: number): void {
-  const sleeper = new Int32Array(new SharedArrayBuffer(4));
-  Atomics.wait(sleeper, 0, 0, milliseconds);
-}
-
-function testBarrier(point: string): void {
-  const dir = process.env.MARKETPLACE_LEASE_BARRIER_DIR;
-  if (!dir) return;
-  const waitPath = path.join(dir, `${point}.wait`);
-  try {
-    fs.statSync(waitPath);
-  } catch {
-    return;
-  }
-  writeAtomic(path.join(dir, `${point}.reached`), `${process.pid}\n`);
-  const goPath = path.join(dir, `${point}.go`);
-  while (true) {
-    try {
-      fs.statSync(goPath);
-      return;
-    } catch {
-      sleepSync(10);
+      // A failed temporary-file cleanup cannot invalidate the target.
     }
     }
   }
   }
 }
 }
@@ -133,461 +119,262 @@ export function normalizeLockOptions(
   if (
   if (
     !Number.isFinite(options.staleMs) ||
     !Number.isFinite(options.staleMs) ||
     !Number.isFinite(options.timeoutMs) ||
     !Number.isFinite(options.timeoutMs) ||
-    !Number.isFinite(options.heartbeatMs) ||
     !Number.isFinite(options.retryMs) ||
     !Number.isFinite(options.retryMs) ||
     options.staleMs <= 0 ||
     options.staleMs <= 0 ||
     options.timeoutMs < 0 ||
     options.timeoutMs < 0 ||
-    options.heartbeatMs <= 0 ||
-    options.retryMs <= 0 ||
-    options.heartbeatMs >= options.staleMs
+    options.retryMs <= 0
   ) {
   ) {
     throw new MarketplaceValidationError(
     throw new MarketplaceValidationError(
-      'Marketplace lock timing requires finite positive stale, heartbeat, and retry intervals; timeout must be non-negative and heartbeat must be shorter than stale timeout',
+      'Marketplace lock timing requires finite positive stale and retry intervals; timeout must be non-negative',
     );
     );
   }
   }
   return {
   return {
     staleMs: options.staleMs,
     staleMs: options.staleMs,
     timeoutMs: options.timeoutMs,
     timeoutMs: options.timeoutMs,
-    heartbeatMs: options.heartbeatMs,
     retryMs: options.retryMs,
     retryMs: options.retryMs,
   };
   };
 }
 }
 
 
-export function generationsRoot(directory: string): string {
-  return path.join(directory, 'gen');
+function lockEntryName(
+  pid: number,
+  uuid: string,
+  kind: LockEntry['kind'],
+): string {
+  return `${pid}.${uuid}.${kind}`;
 }
 }
 
 
-export function generationPath(directory: string, generation: string): string {
-  return path.join(directory, 'gen', generation);
-}
-
-export function heartbeatPath(directory: string, generation: string): string {
-  return path.join(generationPath(directory, generation), 'heartbeat');
+function parseEntryName(
+  name: string,
+): Pick<LockEntry, 'kind' | 'pid' | 'uuid'> | undefined {
+  const match = ENTRY_PATTERN.exec(name);
+  if (!match) return undefined;
+  const pid = Number(match[1]);
+  if (!Number.isSafeInteger(pid)) return undefined;
+  return {
+    pid,
+    uuid: match[2],
+    kind: match[3] as LockEntry['kind'],
+  };
 }
 }
 
 
-function readLeaseMetadata(directory: string): LeaseMetadata | undefined {
+function scanLockDirectory(lockDir: string): LockState {
+  let names: string[];
   try {
   try {
-    const value = JSON.parse(
-      fs.readFileSync(path.join(directory, 'meta.json'), 'utf8'),
-    ) as Partial<LeaseMetadata>;
-    if (
-      !Number.isInteger(value.pid) ||
-      typeof value.generation !== 'string' ||
-      value.generation.length === 0 ||
-      typeof value.token !== 'string' ||
-      value.token.length === 0
-    ) {
-      return undefined;
+    names = fs.readdirSync(lockDir);
+  } catch (error) {
+    if (errnoCode(error) === 'ENOENT') {
+      return { candidates: [], leases: [] };
     }
     }
-    return value as LeaseMetadata;
-  } catch {
-    return undefined;
+    throw new MarketplaceLockfileError(
+      `Unable to read marketplace lock directory: ${String(error)}`,
+    );
   }
   }
-}
 
 
-export function readGenerationState(
-  generationDirectory: string,
-): LeaseState | undefined {
-  const metadata = readLeaseMetadata(generationDirectory);
-  if (!metadata) return undefined;
-  try {
-    const heartbeat = fs.statSync(path.join(generationDirectory, 'heartbeat'));
-    if (!heartbeat.isFile()) return undefined;
-    if (path.basename(generationDirectory) !== metadata.generation) {
-      return undefined;
+  const state: LockState = { candidates: [], leases: [] };
+  for (const name of names) {
+    const parsed = parseEntryName(name);
+    if (!parsed) {
+      throw new MarketplaceLockfileError(
+        `Malformed marketplace lock entry: ${name}`,
+      );
     }
     }
-    return { metadata, heartbeatMtimeMs: heartbeat.mtimeMs };
-  } catch {
-    return undefined;
-  }
-}
-
-export function listValidGenerations(directory: string): LeaseState[] {
-  const root = generationsRoot(directory);
-  let entries: fs.Dirent[];
-  try {
-    entries = fs.readdirSync(root, { withFileTypes: true });
-  } catch (error) {
-    if (errnoCode(error) === 'ENOENT') return [];
-    throw error;
-  }
-  const states: LeaseState[] = [];
-  for (const entry of entries) {
-    if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
-    const state = readGenerationState(path.join(root, entry.name));
-    if (state) states.push(state);
+    const entryPath = path.join(lockDir, name);
+    let stat: fs.Stats;
+    try {
+      stat = fs.lstatSync(entryPath);
+    } catch (error) {
+      if (errnoCode(error) === 'ENOENT') {
+        throw new RestartDirectoryScan();
+      }
+      throw new MarketplaceLockfileError(
+        `Unable to inspect marketplace lock entry ${name}: ${String(error)}`,
+      );
+    }
+    if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== 0) {
+      throw new MarketplaceLockfileError(
+        `Marketplace lock entry is not an empty regular file: ${name}`,
+      );
+    }
+    const entry: LockEntry = {
+      ...parsed,
+      name,
+      path: entryPath,
+      mtimeMs: stat.mtimeMs,
+    };
+    state[entry.kind === 'candidate' ? 'candidates' : 'leases'].push(entry);
   }
   }
-  return states;
-}
-
-export function readLeaseState(directory: string): LeaseState | undefined {
-  const gens = listValidGenerations(directory);
-  if (gens.length !== 1) return undefined;
-  return gens[0];
-}
-
-function sameLease(
-  left: LeaseIdentity | undefined,
-  right: LeaseIdentity,
-): boolean {
-  return Boolean(
-    left && left.generation === right.generation && left.token === right.token,
-  );
+  return state;
 }
 }
 
 
-function processIsAlive(pid: number): boolean {
-  if (pid <= 0) return false;
-  try {
-    process.kill(pid, 0);
-    return true;
-  } catch {
-    return false;
+function readLockDirectory(lockDir: string): LockState {
+  for (let attempt = 0; attempt < MAX_SCAN_RETRIES; attempt++) {
+    try {
+      return scanLockDirectory(lockDir);
+    } catch (error) {
+      if (!(error instanceof RestartDirectoryScan)) throw error;
+    }
   }
   }
-}
-
-export function isHeartbeatStale(
-  state: LeaseState,
-  staleMs: number,
-  nowMs = Date.now(),
-): boolean {
-  return nowMs - state.heartbeatMtimeMs > staleMs;
-}
-
-function breakerIsAbandoned(
-  state: LeaseState,
-  staleMs: number,
-  nowMs: number,
-): boolean {
-  return (
-    isHeartbeatStale(state, staleMs, nowMs) &&
-    !processIsAlive(state.metadata.pid)
+  throw new MarketplaceLockfileError(
+    'Marketplace lock directory changed during every scan attempt',
   );
   );
 }
 }
 
 
-function creatingPid(name: string): number | undefined {
-  if (!name.startsWith(CREATING_PREFIX)) return undefined;
-  const pid = Number(name.slice(CREATING_PREFIX.length).split('.')[0]);
-  return Number.isInteger(pid) ? pid : undefined;
-}
-
-function isAbandonedCreating(
-  fullPath: string,
-  name: string,
-  staleMs: number,
-  nowMs: number,
-): boolean {
-  const pid = creatingPid(name);
-  if (pid === undefined) return false;
-  if (!processIsAlive(pid)) return true;
-  try {
-    return nowMs - fs.statSync(fullPath).mtimeMs > staleMs;
-  } catch {
-    return false;
-  }
-}
-
-function cleanAbandonedCreating(
-  directory: string,
-  staleMs: number,
-  nowMs: number,
-): void {
-  let entries: fs.Dirent[];
+function unlinkExact(filePath: string): void {
   try {
   try {
-    entries = fs.readdirSync(directory, { withFileTypes: true });
+    fs.unlinkSync(filePath);
+    syncDirectory(path.dirname(filePath));
   } catch (error) {
   } catch (error) {
     if (errnoCode(error) === 'ENOENT') return;
     if (errnoCode(error) === 'ENOENT') return;
     throw error;
     throw error;
   }
   }
-  for (const entry of entries) {
-    if (!entry.isDirectory()) continue;
-    const fullPath = path.join(directory, entry.name);
-    if (isAbandonedCreating(fullPath, entry.name, staleMs, nowMs)) {
-      fs.rmSync(fullPath, { recursive: true, force: true });
-    }
-  }
-}
-
-function cleanAbandonedArtifacts(
-  directory: string,
-  staleMs: number,
-  nowMs: number,
-): void {
-  fs.mkdirSync(directory, { recursive: true });
-  cleanAbandonedCreating(directory, staleMs, nowMs);
-  cleanAbandonedCreating(generationsRoot(directory), staleMs, nowMs);
 }
 }
 
 
-/**
- * Delete only `gen/<expected.generation>` after an in-place identity check.
- * A replacement lives at a different path and cannot be targeted.
- */
-function deleteExpectedGeneration(
-  directory: string,
-  expected: LeaseIdentity,
-): boolean {
-  const target = generationPath(directory, expected.generation);
-  if (!sameLease(readGenerationState(target)?.metadata, expected)) {
-    return false;
+function cleanupAgedEntries(lockDir: string, staleMs: number): LockState {
+  const state = readLockDirectory(lockDir);
+  const now = Date.now();
+  for (const candidate of state.candidates) {
+    if (now - candidate.mtimeMs > staleMs) unlinkExact(candidate.path);
+  }
+  for (const lease of state.leases) {
+    if (now - lease.mtimeMs > staleMs && pidIsDead(lease.pid)) {
+      unlinkExact(lease.path);
+    }
   }
   }
-  fs.rmSync(target, { recursive: true, force: true });
-  return true;
+  return readLockDirectory(lockDir);
+}
+
+function createCandidate(
+  lockDir: string,
+  afterCandidateCreated?: (candidatePath: string) => void,
+): LockEntry {
+  const uuid = randomUUID();
+  const pid = process.pid;
+  const name = lockEntryName(pid, uuid, 'candidate');
+  const candidate: LockEntry = {
+    kind: 'candidate',
+    name,
+    path: path.join(lockDir, name),
+    pid,
+    uuid,
+    mtimeMs: Date.now(),
+  };
+  const fd = fs.openSync(candidate.path, 'wx', 0o600);
+  fs.closeSync(fd);
+  syncDirectory(lockDir);
+  afterCandidateCreated?.(candidate.path);
+  return candidate;
 }
 }
 
 
-function deleteStaleGeneration(
-  directory: string,
-  expected: LeaseIdentity,
-  staleMs: number,
-  requireDeadPid: boolean,
-  nowMs: number,
-): boolean {
-  const target = generationPath(directory, expected.generation);
-  const state = readGenerationState(target);
-  if (!sameLease(state?.metadata, expected) || !state) return false;
-  if (!isHeartbeatStale(state, staleMs, nowMs)) return false;
-  if (requireDeadPid && processIsAlive(state.metadata.pid)) return false;
-  const again = readGenerationState(target);
-  if (!sameLease(again?.metadata, expected) || !again) return false;
-  if (!isHeartbeatStale(again, staleMs, nowMs)) return false;
-  if (requireDeadPid && processIsAlive(again.metadata.pid)) return false;
-  fs.rmSync(target, { recursive: true, force: true });
-  return true;
+function candidateToLease(candidate: LockEntry): LockEntry {
+  const name = lockEntryName(candidate.pid, candidate.uuid, 'lease');
+  return {
+    ...candidate,
+    kind: 'lease',
+    name,
+    path: path.join(path.dirname(candidate.path), name),
+  };
 }
 }
 
 
-function prepareGeneration(directory: string, identity: LeaseIdentity): string {
-  fs.mkdirSync(generationsRoot(directory), { recursive: true });
-  const privateDirectory = path.join(
-    directory,
-    `${CREATING_PREFIX}${process.pid}.${randomUUID()}`,
-  );
-  fs.mkdirSync(privateDirectory, { recursive: false, mode: 0o700 });
-  testBarrier('prepare-generation');
+function acquireOnce(
+  lockDir: string,
+  options: MarketplaceLockOptions,
+  hooks: MarketplaceLeaseTestHooks,
+): LockEntry | undefined {
+  const candidate = createCandidate(lockDir, hooks.afterCandidateCreated);
+  const ownLease = candidateToLease(candidate);
   try {
   try {
-    writeAtomic(
-      path.join(privateDirectory, 'meta.json'),
-      `${JSON.stringify({ ...identity, pid: process.pid })}\n`,
-    );
-    writeAtomic(path.join(privateDirectory, 'heartbeat'), '');
-    syncDirectory(privateDirectory);
-    return privateDirectory;
-  } catch {
-    fs.rmSync(privateDirectory, { recursive: true, force: true });
-    throw new MarketplaceLockfileError(
-      'Failed to initialize a private marketplace lease generation',
-    );
-  }
-}
+    const electedState = cleanupAgedEntries(lockDir, options.staleMs);
+    const elected = electedState.candidates;
+    elected.sort((left, right) => left.name.localeCompare(right.name));
+    if (
+      electedState.leases.length !== 0 ||
+      elected.length !== 1 ||
+      elected[0]?.path !== candidate.path
+    ) {
+      unlinkExact(candidate.path);
+      return undefined;
+    }
 
 
-function publishPreparedGeneration(
-  privateDirectory: string,
-  directory: string,
-  generation: string,
-): boolean {
-  const dest = generationPath(directory, generation);
-  try {
-    fs.renameSync(privateDirectory, dest);
-    syncDirectory(generationsRoot(directory));
-    return true;
-  } catch (error) {
-    const code = errnoCode(error);
-    if (code === 'EEXIST' || code === 'ENOTEMPTY' || code === 'EISDIR') {
-      return false;
+    try {
+      fs.renameSync(candidate.path, ownLease.path);
+      syncDirectory(lockDir);
+    } catch (error) {
+      if (errnoCode(error) === 'ENOENT') return undefined;
+      throw new MarketplaceLockfileError(
+        `Unable to publish marketplace lease: ${String(error)}`,
+      );
     }
     }
-    throw error;
-  } finally {
+
+    const finalState = readLockDirectory(lockDir);
+    if (
+      finalState.leases.length !== 1 ||
+      finalState.leases[0]?.path !== ownLease.path
+    ) {
+      unlinkExact(ownLease.path);
+      throw new MarketplaceLockfileError(
+        'Ambiguous marketplace lock state after lease publication',
+      );
+    }
+    return finalState.leases[0];
+  } catch (error) {
     try {
     try {
-      if (fs.existsSync(privateDirectory)) {
-        fs.rmSync(privateDirectory, { recursive: true, force: true });
-      }
+      if (fs.existsSync(candidate.path)) unlinkExact(candidate.path);
     } catch {
     } catch {
-      // An unpublished private generation is never an active lease.
+      // The original acquisition error is authoritative.
     }
     }
+    throw error;
   }
   }
 }
 }
 
 
-export function publishGeneration(
-  directory: string,
-  identity: LeaseIdentity,
-): boolean {
-  const privateDirectory = prepareGeneration(directory, identity);
-  return publishPreparedGeneration(
-    privateDirectory,
-    directory,
-    identity.generation,
-  );
-}
-
-function assertFreshAndCurrent(
-  directory: string,
-  identity: LeaseIdentity,
-  staleMs: number,
-  nowMs: number,
-): void {
-  const state = readGenerationState(
-    generationPath(directory, identity.generation),
-  );
-  if (
-    !sameLease(state?.metadata, identity) ||
-    !state ||
-    isHeartbeatStale(state, staleMs, nowMs)
-  ) {
-    throw new MarketplaceLockOwnershipError(OWNERSHIP_MESSAGE);
-  }
-}
-
-function assertCurrentIdentity(
-  directory: string,
-  identity: LeaseIdentity,
-): void {
-  if (
-    !sameLease(
-      readGenerationState(generationPath(directory, identity.generation))
-        ?.metadata,
-      identity,
-    )
-  ) {
-    throw new MarketplaceLockOwnershipError(OWNERSHIP_MESSAGE);
-  }
-}
-
-function touchHeartbeat(directory: string, identity: LeaseIdentity): void {
+function exactLeaseExists(lease: LockEntry): boolean {
   try {
   try {
-    const now = new Date();
-    fs.utimesSync(heartbeatPath(directory, identity.generation), now, now);
+    const stat = fs.lstatSync(lease.path);
+    return stat.isFile() && !stat.isSymbolicLink() && stat.size === 0;
   } catch {
   } catch {
-    throw new MarketplaceLockOwnershipError(OWNERSHIP_MESSAGE);
+    return false;
   }
   }
 }
 }
 
 
-function startHeartbeat(
-  directory: string,
-  identity: LeaseIdentity,
-  heartbeatMs: number,
-): ReturnType<typeof setInterval> {
-  const beat = () => {
-    try {
-      const now = new Date();
-      fs.utimesSync(heartbeatPath(directory, identity.generation), now, now);
-    } catch {
-      // Release and commit perform authoritative ownership checks.
-    }
-  };
-  beat();
-  const timer = setInterval(beat, heartbeatMs);
-  timer.unref?.();
-  return timer;
-}
+function createLease(lockDir: string, leaseEntry: LockEntry): MarketplaceLease {
+  let status: 'owned' | 'released' | 'lost' = 'owned';
 
 
-function newIdentity(): LeaseIdentity {
-  return {
-    generation: randomUUID(),
-    token: `${process.pid}:${randomUUID()}`,
+  const lost = (): MarketplaceLockOwnershipError => {
+    status = 'lost';
+    return new MarketplaceLockOwnershipError(
+      'Marketplace lease ownership is no longer current',
+    );
   };
   };
-}
 
 
-function acquireBreakerLease(
-  paths: MarketplacePaths,
-  options: MarketplaceLockOptions,
-  now?: () => number,
-): BreakerLease {
-  const deadline = Date.now() + options.timeoutMs;
-  while (Date.now() <= deadline) {
-    const nowMs = lockNow(now);
-    cleanAbandonedArtifacts(paths.breakerDir, options.staleMs, nowMs);
-    for (const state of listValidGenerations(paths.breakerDir)) {
-      if (breakerIsAbandoned(state, options.staleMs, nowMs)) {
-        deleteStaleGeneration(
-          paths.breakerDir,
-          state.metadata,
-          options.staleMs,
-          true,
-          nowMs,
-        );
-      }
-    }
-    const live = listValidGenerations(paths.breakerDir).filter(
-      (state) => !breakerIsAbandoned(state, options.staleMs, nowMs),
-    );
-    if (live.length > 0) {
-      sleepSync(options.retryMs);
-      continue;
-    }
-    const identity = newIdentity();
-    if (!publishGeneration(paths.breakerDir, identity)) {
-      sleepSync(options.retryMs);
-      continue;
-    }
-    testBarrier('breaker-critical');
-    const others = listValidGenerations(paths.breakerDir).filter(
-      (state) => state.metadata.generation !== identity.generation,
-    );
-    if (others.length > 0) {
-      deleteExpectedGeneration(paths.breakerDir, identity);
-      sleepSync(options.retryMs);
-      continue;
+  const assertCurrent = (): void => {
+    if (status !== 'owned') throw lost();
+    const state = readLockDirectory(lockDir);
+    if (
+      state.leases.length !== 1 ||
+      state.leases[0]?.path !== leaseEntry.path ||
+      !exactLeaseExists(leaseEntry)
+    ) {
+      throw lost();
     }
     }
-    const heartbeatTimer = startHeartbeat(
-      paths.breakerDir,
-      identity,
-      options.heartbeatMs,
-    );
-    return {
-      release: () => {
-        clearInterval(heartbeatTimer);
-        if (!deleteExpectedGeneration(paths.breakerDir, identity)) {
-          throw new MarketplaceLockOwnershipError(
-            'Refusing to release a compromised marketplace breaker',
-          );
-        }
-      },
-    };
-  }
-  throw new MarketplaceBusyError('Timed out waiting for marketplace breaker');
-}
-
-function createMarketplaceLease(
-  paths: MarketplacePaths,
-  identity: LeaseIdentity,
-  options: MarketplaceLockOptions,
-  now?: () => number,
-): MarketplaceLease {
-  let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
-  const assertCurrent = () => {
-    assertFreshAndCurrent(
-      paths.lockDir,
-      identity,
-      options.staleMs,
-      lockNow(now),
-    );
   };
   };
-  const release = () => {
-    if (heartbeatTimer) clearInterval(heartbeatTimer);
-    const breaker = acquireBreakerLease(paths, options, now);
+
+  const release = (): void => {
+    if (status !== 'owned') throw lost();
     try {
     try {
-      if (!deleteExpectedGeneration(paths.lockDir, identity)) {
-        throw new MarketplaceLockOwnershipError(
-          'Refusing to release a compromised marketplace lock',
-        );
-      }
-    } finally {
-      breaker.release();
+      fs.unlinkSync(leaseEntry.path);
+      syncDirectory(lockDir);
+      status = 'released';
+    } catch (error) {
+      if (errnoCode(error) === 'ENOENT') throw lost();
+      if (exactLeaseExists(leaseEntry)) throw error;
+      throw lost();
     }
     }
   };
   };
-  heartbeatTimer = startHeartbeat(paths.lockDir, identity, options.heartbeatMs);
+
   return {
   return {
-    ...identity,
     assertCurrent,
     assertCurrent,
-    commit: <T>(operation: () => T): T => {
-      const breaker = acquireBreakerLease(paths, options, now);
-      try {
-        assertCurrent();
-        touchHeartbeat(paths.lockDir, identity);
-        const result = operation();
-        assertCurrentIdentity(paths.lockDir, identity);
-        touchHeartbeat(paths.lockDir, identity);
-        return result;
-      } finally {
-        breaker.release();
-      }
+    commit<T>(operation: () => T): T {
+      assertCurrent();
+      return operation();
     },
     },
     release,
     release,
   };
   };
@@ -596,52 +383,15 @@ function createMarketplaceLease(
 function acquireMarketplaceLeaseInternal(
 function acquireMarketplaceLeaseInternal(
   paths: MarketplacePaths,
   paths: MarketplacePaths,
   partialOptions: Partial<MarketplaceLockOptions> = {},
   partialOptions: Partial<MarketplaceLockOptions> = {},
-  now?: () => number,
+  hooks: MarketplaceLeaseTestHooks = {},
 ): MarketplaceLease {
 ): MarketplaceLease {
   const options = normalizeLockOptions(partialOptions);
   const options = normalizeLockOptions(partialOptions);
   fs.mkdirSync(paths.rootDir, { recursive: true });
   fs.mkdirSync(paths.rootDir, { recursive: true });
+  fs.mkdirSync(paths.lockDir, { recursive: true });
   const deadline = Date.now() + options.timeoutMs;
   const deadline = Date.now() + options.timeoutMs;
   while (Date.now() <= deadline) {
   while (Date.now() <= deadline) {
-    let breaker: BreakerLease | undefined;
-    try {
-      breaker = acquireBreakerLease(
-        paths,
-        {
-          ...options,
-          timeoutMs: Math.max(0, deadline - Date.now()),
-        },
-        now,
-      );
-      const nowMs = lockNow(now);
-      cleanAbandonedArtifacts(paths.lockDir, options.staleMs, nowMs);
-      for (const state of listValidGenerations(paths.lockDir)) {
-        if (isHeartbeatStale(state, options.staleMs, nowMs)) {
-          deleteStaleGeneration(
-            paths.lockDir,
-            state.metadata,
-            options.staleMs,
-            false,
-            nowMs,
-          );
-        }
-      }
-      const live = listValidGenerations(paths.lockDir).filter(
-        (state) => !isHeartbeatStale(state, options.staleMs, nowMs),
-      );
-      if (live.length === 0) {
-        const identity = newIdentity();
-        if (publishGeneration(paths.lockDir, identity)) {
-          const lease = createMarketplaceLease(paths, identity, options, now);
-          breaker.release();
-          breaker = undefined;
-          return lease;
-        }
-      }
-    } catch (error) {
-      if (!(error instanceof MarketplaceBusyError)) throw error;
-    } finally {
-      breaker?.release();
-    }
+    const lease = acquireOnce(paths.lockDir, options, hooks);
+    if (lease) return createLease(paths.lockDir, lease);
     if (Date.now() + options.retryMs > deadline) break;
     if (Date.now() + options.retryMs > deadline) break;
     sleepSync(options.retryMs);
     sleepSync(options.retryMs);
   }
   }
@@ -651,17 +401,9 @@ function acquireMarketplaceLeaseInternal(
 export function acquireMarketplaceLease(
 export function acquireMarketplaceLease(
   paths: MarketplacePaths,
   paths: MarketplacePaths,
   partialOptions: Partial<MarketplaceLockOptions> = {},
   partialOptions: Partial<MarketplaceLockOptions> = {},
+  hooks: MarketplaceLeaseTestHooks = {},
 ): MarketplaceLease {
 ): MarketplaceLease {
-  return acquireMarketplaceLeaseInternal(paths, partialOptions);
-}
-
-/** Test-only clock seam. Production store/lock options cannot pass `now`. */
-export function acquireMarketplaceLeaseForTests(
-  paths: MarketplacePaths,
-  partialOptions: Partial<MarketplaceLockOptions>,
-  now: () => number,
-): MarketplaceLease {
-  return acquireMarketplaceLeaseInternal(paths, partialOptions, now);
+  return acquireMarketplaceLeaseInternal(paths, partialOptions, hooks);
 }
 }
 
 
 export function withMarketplaceLease<T>(
 export function withMarketplaceLease<T>(

+ 0 - 2
src/marketplace/paths.ts

@@ -6,7 +6,6 @@ export interface MarketplacePaths {
   packagesDir: string;
   packagesDir: string;
   lockfilePath: string;
   lockfilePath: string;
   lockDir: string;
   lockDir: string;
-  breakerDir: string;
   stagingDir: string;
   stagingDir: string;
 }
 }
 
 
@@ -25,7 +24,6 @@ export function getMarketplacePaths(rootDir?: string): MarketplacePaths {
     packagesDir: join(root, 'packages'),
     packagesDir: join(root, 'packages'),
     lockfilePath: join(root, 'lock.json'),
     lockfilePath: join(root, 'lock.json'),
     lockDir: join(root, 'marketplace.lock'),
     lockDir: join(root, 'marketplace.lock'),
-    breakerDir: join(root, 'marketplace.lock.breaker'),
     stagingDir: join(root, '.staging'),
     stagingDir: join(root, '.staging'),
   };
   };
 }
 }

+ 10 - 14
src/marketplace/r1-dispatch.test.ts

@@ -13,30 +13,26 @@ import { MarketplaceService } from './service';
 function bundle(version = '1.0.0'): MarketplacePackageBundle {
 function bundle(version = '1.0.0'): MarketplacePackageBundle {
   return {
   return {
     manifest: {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/dispatch-agent',
       id: 'community/dispatch-agent',
       version,
       version,
-      kind: 'agent',
       displayName: 'Dispatch agent',
       displayName: 'Dispatch agent',
       description: 'A dispatch test package.',
       description: 'A dispatch test package.',
-      instructions: 'Use the explorer role.',
+      agentName: 'dispatchagent',
+      prompt: 'Use the explorer role.',
       author: { name: 'Community' },
       author: { name: 'Community' },
       tags: ['dispatch'],
       tags: ['dispatch'],
       license: 'MIT',
       license: 'MIT',
-      compatibility: { plugin: '>=3.0.0', roleContract: '^1.0.0' },
+      compatibility: { plugin: '>=3.0.0' },
       routing: {
       routing: {
         description: 'Dispatch test routing.',
         description: 'Dispatch test routing.',
         keywords: ['dispatch'],
         keywords: ['dispatch'],
-        delegation: { when: 'When dispatching.', preferredRoles: [] },
-      },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
+        when: 'When dispatching.',
       },
       },
-      capabilities: { tools: [], permissions: [] },
-      baseRole: 'explorer',
-      agentName: 'dispatchagent',
-      overrides: {},
+      skills: [],
+      mcps: [],
+      tools: [],
+      model: { source: 'explicit', candidates: ['provider/model'] },
     },
     },
   };
   };
 }
 }
@@ -47,7 +43,7 @@ function download(version: string): MarketplaceRegistryDownload {
     bundle: packageBundle,
     bundle: packageBundle,
     entry: createMarketplaceRegistryEntry(packageBundle),
     entry: createMarketplaceRegistryEntry(packageBundle),
     indexUrl: MARKETPLACE_REGISTRY_INDEX_URL,
     indexUrl: MARKETPLACE_REGISTRY_INDEX_URL,
-    packageUrl: `https://registry.ohmyopencodeslim.com/v1/artifacts/community/dispatch-agent/${version}.json`,
+    packageUrl: `https://registry.ohmyopencodeslim.com/v2/artifacts/community/dispatch-agent/${version}.json`,
   };
   };
 }
 }
 
 

+ 49 - 47
src/marketplace/registry-client.test.ts

@@ -30,6 +30,12 @@ import {
 } from './registry-client';
 } from './registry-client';
 import { MarketplaceService } from './service';
 import { MarketplaceService } from './service';
 
 
+const canonicalRetirements = [
+  { id: 'alvin/deepwork-implementer' },
+  { id: 'alvin/deepwork-recon' },
+  { id: 'alvin/deepwork-reviewer' },
+];
+
 function bundle(
 function bundle(
   version = '1.0.0',
   version = '1.0.0',
   id = 'community/registry-agent',
   id = 'community/registry-agent',
@@ -37,33 +43,26 @@ function bundle(
 ): MarketplacePackageBundle {
 ): MarketplacePackageBundle {
   return {
   return {
     manifest: {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id,
       id,
       version,
       version,
-      kind: 'agent',
       displayName: 'Registry agent',
       displayName: 'Registry agent',
       description: 'A registry test package.',
       description: 'A registry test package.',
-      instructions: 'Use the explorer role.',
+      agentName: 'registryagent',
+      prompt: 'Use the explorer role.',
       author: { name: 'Community' },
       author: { name: 'Community' },
       tags: ['registry'],
       tags: ['registry'],
       license: 'MIT',
       license: 'MIT',
-      compatibility: { plugin, roleContract: '^1.0.0' },
+      compatibility: { plugin },
       routing: {
       routing: {
         description: 'Explore registry fixtures.',
         description: 'Explore registry fixtures.',
         keywords: ['registry'],
         keywords: ['registry'],
-        delegation: {
-          when: 'When testing registry installs.',
-          preferredRoles: [],
-        },
+        when: 'When testing registry installs.',
       },
       },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
-      },
-      capabilities: { tools: [], permissions: [] },
-      baseRole: 'explorer',
-      agentName: 'registryagent',
-      overrides: {},
+      skills: [],
+      mcps: [],
+      tools: [],
+      model: { source: 'explicit', candidates: ['provider/model'] },
     },
     },
   };
   };
 }
 }
@@ -71,7 +70,7 @@ function bundle(
 function indexFor(packageBundle = bundle()): Record<string, unknown> {
 function indexFor(packageBundle = bundle()): Record<string, unknown> {
   const manifest = packageBundle.manifest;
   const manifest = packageBundle.manifest;
   return {
   return {
-    schemaVersion: 1,
+    schemaVersion: 3,
     entries: [
     entries: [
       {
       {
         id: manifest.id,
         id: manifest.id,
@@ -79,12 +78,13 @@ function indexFor(packageBundle = bundle()): Record<string, unknown> {
         artifactPath: registryArtifactPath(manifest.id, manifest.version),
         artifactPath: registryArtifactPath(manifest.id, manifest.version),
         digest: {
         digest: {
           algorithm: 'sha256',
           algorithm: 'sha256',
-          domain: 'marketplace-bundle-v1',
+          domain: 'marketplace-agent-bundle-v2',
           value: digestMarketplaceBundle(packageBundle),
           value: digestMarketplaceBundle(packageBundle),
         },
         },
         summary: projectMarketplaceManifestSummary(manifest),
         summary: projectMarketplaceManifestSummary(manifest),
       },
       },
     ],
     ],
+    retirements: canonicalRetirements,
   };
   };
 }
 }
 
 
@@ -100,11 +100,12 @@ describe('marketplace registry contract', () => {
     const first = indexFor(bundle('1.0.0'));
     const first = indexFor(bundle('1.0.0'));
     const second = indexFor(bundle('2.0.0'));
     const second = indexFor(bundle('2.0.0'));
     const validIndex = {
     const validIndex = {
-      schemaVersion: 1,
+      schemaVersion: 3,
       entries: [
       entries: [
         ...(first.entries as unknown[]),
         ...(first.entries as unknown[]),
         ...(second.entries as unknown[]),
         ...(second.entries as unknown[]),
       ],
       ],
+      retirements: canonicalRetirements,
     };
     };
     expect(MarketplaceRegistryIndexSchema.safeParse(validIndex).success).toBe(
     expect(MarketplaceRegistryIndexSchema.safeParse(validIndex).success).toBe(
       true,
       true,
@@ -150,17 +151,18 @@ describe('marketplace registry contract', () => {
       version: '2.0.0',
       version: '2.0.0',
     });
     });
     const index = parseMarketplaceRegistryIndex({
     const index = parseMarketplaceRegistryIndex({
-      schemaVersion: 1,
+      schemaVersion: 3,
       entries: [
       entries: [
         ...(indexFor(bundle('1.0.0')).entries as unknown[]),
         ...(indexFor(bundle('1.0.0')).entries as unknown[]),
         ...(indexFor(bundle('2.0.0')).entries as unknown[]),
         ...(indexFor(bundle('2.0.0')).entries as unknown[]),
       ],
       ],
+      retirements: canonicalRetirements,
     });
     });
     expect(
     expect(
       resolveMarketplaceRegistryEntry(
       resolveMarketplaceRegistryEntry(
         index,
         index,
         { id: 'community/registry-agent' },
         { id: 'community/registry-agent' },
-        { pluginVersion: '3.1.0', roleContractVersion: '1.0.0' },
+        { pluginVersion: '3.1.0' },
       ).version,
       ).version,
     ).toBe('2.0.0');
     ).toBe('2.0.0');
     expect(() =>
     expect(() =>
@@ -171,7 +173,7 @@ describe('marketplace registry contract', () => {
     ).toThrow();
     ).toThrow();
   });
   });
 
 
-  test('validates v2 retirement tombstones and rejects retired selectors', () => {
+  test('validates v3 retirement tombstones and rejects retired selectors', () => {
     const first = indexFor(bundle('1.0.0', 'community/alpha'));
     const first = indexFor(bundle('1.0.0', 'community/alpha'));
     const second = indexFor(bundle('1.0.0', 'community/beta'));
     const second = indexFor(bundle('1.0.0', 'community/beta'));
     const entries = [
     const entries = [
@@ -180,46 +182,46 @@ describe('marketplace registry contract', () => {
     ];
     ];
     expect(
     expect(
       parseMarketplaceRegistryIndex({
       parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
+        schemaVersion: 3,
         entries,
         entries,
-        retirements: [{ id: 'community/alpha' }],
+        retirements: [...canonicalRetirements, { id: 'community/alpha' }],
       }).schemaVersion,
       }).schemaVersion,
-    ).toBe(2);
+    ).toBe(3);
     expect(() =>
     expect(() =>
       parseMarketplaceRegistryIndex({
       parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
+        schemaVersion: 3,
         entries,
         entries,
-        retirements: [{ id: 'community/missing' }],
-      }),
-    ).toThrow('no registry entry');
-    expect(() =>
-      parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
-        entries,
-        retirements: [{ id: 'community/alpha' }, { id: 'community/alpha' }],
+        retirements: [
+          ...canonicalRetirements,
+          { id: 'community/alpha' },
+          { id: 'community/alpha' },
+        ],
       }),
       }),
     ).toThrow('Duplicate');
     ).toThrow('Duplicate');
     expect(() =>
     expect(() =>
       parseMarketplaceRegistryIndex({
       parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
+        schemaVersion: 3,
         entries,
         entries,
-        retirements: [{ id: 'community/beta' }, { id: 'community/alpha' }],
+        retirements: [
+          ...canonicalRetirements,
+          { id: 'community/beta' },
+          { id: 'community/alpha' },
+        ],
       }),
       }),
     ).toThrow('sorted');
     ).toThrow('sorted');
 
 
     const index = parseMarketplaceRegistryIndex({
     const index = parseMarketplaceRegistryIndex({
-      schemaVersion: 2,
+      schemaVersion: 3,
       entries,
       entries,
-      retirements: [{ id: 'community/alpha' }],
+      retirements: canonicalRetirements,
     });
     });
     for (const selector of [
     for (const selector of [
-      { id: 'community/alpha' },
-      { id: 'community/alpha', version: '1.0.0' },
+      { id: 'alvin/deepwork-implementer' },
+      { id: 'alvin/deepwork-implementer', version: '1.0.0' },
     ]) {
     ]) {
       expect(() =>
       expect(() =>
         resolveMarketplaceRegistryEntry(index, selector, {
         resolveMarketplaceRegistryEntry(index, selector, {
           pluginVersion: '3.1.0',
           pluginVersion: '3.1.0',
-          roleContractVersion: '1.0.0',
         }),
         }),
       ).toThrow(MarketplaceRetiredError);
       ).toThrow(MarketplaceRetiredError);
     }
     }
@@ -265,14 +267,14 @@ describe('MarketplaceRegistryClient', () => {
         service.store.getLockfile().packages['community/registry-agent'].source,
         service.store.getLockfile().packages['community/registry-agent'].source,
       ).toEqual({
       ).toEqual({
         kind: 'registry',
         kind: 'registry',
-        registry: 'https://registry.ohmyopencodeslim.com/v1/',
+        registry: 'https://registry.ohmyopencodeslim.com/v2/',
         indexUrl: MARKETPLACE_REGISTRY_INDEX_URL,
         indexUrl: MARKETPLACE_REGISTRY_INDEX_URL,
         packageUrl:
         packageUrl:
-          'https://registry.ohmyopencodeslim.com/v1/artifacts/community/registry-agent/1.0.0.json',
+          'https://registry.ohmyopencodeslim.com/v2/artifacts/community/registry-agent/1.0.0.json',
       });
       });
       expect(calls).toEqual([
       expect(calls).toEqual([
         MARKETPLACE_REGISTRY_INDEX_URL,
         MARKETPLACE_REGISTRY_INDEX_URL,
-        'https://registry.ohmyopencodeslim.com/v1/artifacts/community/registry-agent/1.0.0.json',
+        'https://registry.ohmyopencodeslim.com/v2/artifacts/community/registry-agent/1.0.0.json',
       ]);
       ]);
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
@@ -372,7 +374,7 @@ describe('MarketplaceRegistryClient', () => {
     const oversized = new MarketplaceRegistryClient({
     const oversized = new MarketplaceRegistryClient({
       pluginVersion: '3.1.0',
       pluginVersion: '3.1.0',
       maxIndexBytes: 10,
       maxIndexBytes: 10,
-      fetch: async () => response({ schemaVersion: 1, entries: [] }),
+      fetch: async () => response({ schemaVersion: 3, entries: [] }),
     });
     });
     await expect(oversized.fetchIndex()).rejects.toBeInstanceOf(
     await expect(oversized.fetchIndex()).rejects.toBeInstanceOf(
       MarketplaceRegistryProtocolError,
       MarketplaceRegistryProtocolError,
@@ -433,12 +435,12 @@ describe('MarketplaceRegistryClient', () => {
   test('preserves the installed state after every remote validation failure', async () => {
   test('preserves the installed state after every remote validation failure', async () => {
     const interruptedBody = new ReadableStream<Uint8Array>({
     const interruptedBody = new ReadableStream<Uint8Array>({
       start(controller) {
       start(controller) {
-        controller.enqueue(new TextEncoder().encode('{"schemaVersion":1'));
+        controller.enqueue(new TextEncoder().encode('{"schemaVersion":3'));
         controller.error(new Error('connection interrupted'));
         controller.error(new Error('connection interrupted'));
       },
       },
     });
     });
     const failures = [
     const failures = [
-      async () => response({ schemaVersion: 1, entries: [] }),
+      async () => response({ schemaVersion: 3, entries: [] }),
       async (input: RequestInfo | URL) => {
       async (input: RequestInfo | URL) => {
         const index = indexFor(bundle());
         const index = indexFor(bundle());
         (index.entries as any)[0].digest.value = '0'.repeat(64);
         (index.entries as any)[0].digest.value = '0'.repeat(64);

+ 10 - 18
src/marketplace/registry-client.ts

@@ -1,6 +1,7 @@
 import { satisfies } from 'semver';
 import { satisfies } from 'semver';
 import {
 import {
   DEFAULT_MARKETPLACE_REGISTRY_URL,
   DEFAULT_MARKETPLACE_REGISTRY_URL,
+  isMarketplaceRegistryIdRetired,
   MarketplacePackageBundleSchema,
   MarketplacePackageBundleSchema,
   type MarketplaceRegistryEntry,
   type MarketplaceRegistryEntry,
   type MarketplaceRegistryIndex,
   type MarketplaceRegistryIndex,
@@ -18,10 +19,7 @@ import {
   MarketplaceRetiredError,
   MarketplaceRetiredError,
 } from './errors';
 } from './errors';
 import { assertMarketplacePackageNotRetired } from './retirements';
 import { assertMarketplacePackageNotRetired } from './retirements';
-import {
-  MARKETPLACE_ROLE_CONTRACT_VERSION,
-  type MarketplacePackageBundle,
-} from './schemas';
+import type { MarketplacePackageBundle } from './schemas';
 
 
 export const MARKETPLACE_REGISTRY_INDEX_URL = `${DEFAULT_MARKETPLACE_REGISTRY_URL}index.json`;
 export const MARKETPLACE_REGISTRY_INDEX_URL = `${DEFAULT_MARKETPLACE_REGISTRY_URL}index.json`;
 export const DEFAULT_MARKETPLACE_REGISTRY_TIMEOUT_MS = 10_000;
 export const DEFAULT_MARKETPLACE_REGISTRY_TIMEOUT_MS = 10_000;
@@ -30,7 +28,6 @@ export const DEFAULT_MARKETPLACE_REGISTRY_ARTIFACT_MAX_BYTES = 512 * 1024;
 
 
 export interface MarketplaceRegistryClientOptions {
 export interface MarketplaceRegistryClientOptions {
   pluginVersion: string;
   pluginVersion: string;
-  roleContractVersion?: string;
   timeoutMs?: number;
   timeoutMs?: number;
   maxIndexBytes?: number;
   maxIndexBytes?: number;
   maxArtifactBytes?: number;
   maxArtifactBytes?: number;
@@ -97,7 +94,6 @@ export class MarketplaceRegistryClient {
   private readonly timeoutMs: number;
   private readonly timeoutMs: number;
   private readonly maxIndexBytes: number;
   private readonly maxIndexBytes: number;
   private readonly maxArtifactBytes: number;
   private readonly maxArtifactBytes: number;
-  private readonly roleContractVersion: string;
 
 
   constructor(private readonly options: MarketplaceRegistryClientOptions) {
   constructor(private readonly options: MarketplaceRegistryClientOptions) {
     this.fetcher = options.fetch ?? globalThis.fetch;
     this.fetcher = options.fetch ?? globalThis.fetch;
@@ -108,8 +104,6 @@ export class MarketplaceRegistryClient {
     this.maxArtifactBytes =
     this.maxArtifactBytes =
       options.maxArtifactBytes ??
       options.maxArtifactBytes ??
       DEFAULT_MARKETPLACE_REGISTRY_ARTIFACT_MAX_BYTES;
       DEFAULT_MARKETPLACE_REGISTRY_ARTIFACT_MAX_BYTES;
-    this.roleContractVersion =
-      options.roleContractVersion ?? MARKETPLACE_ROLE_CONTRACT_VERSION;
   }
   }
 
 
   async fetchIndex(signal?: AbortSignal): Promise<MarketplaceRegistryIndex> {
   async fetchIndex(signal?: AbortSignal): Promise<MarketplaceRegistryIndex> {
@@ -148,6 +142,11 @@ export class MarketplaceRegistryClient {
     })();
     })();
     assertMarketplacePackageNotRetired(selector.id);
     assertMarketplacePackageNotRetired(selector.id);
     const index = await this.fetchIndex(signal);
     const index = await this.fetchIndex(signal);
+    if (isMarketplaceRegistryIdRetired(index, selector.id)) {
+      throw new MarketplaceRetiredError(
+        `${selector.id} is retired and cannot be installed`,
+      );
+    }
     const matchingId = index.entries.some((entry) => entry.id === selector.id);
     const matchingId = index.entries.some((entry) => entry.id === selector.id);
     if (!matchingId) {
     if (!matchingId) {
       throw new MarketplaceRegistryNotFoundError(
       throw new MarketplaceRegistryNotFoundError(
@@ -172,7 +171,6 @@ export class MarketplaceRegistryClient {
         selector,
         selector,
         {
         {
           pluginVersion: this.options.pluginVersion,
           pluginVersion: this.options.pluginVersion,
-          roleContractVersion: this.roleContractVersion,
         },
         },
         minimumVersion,
         minimumVersion,
       );
       );
@@ -224,15 +222,9 @@ export class MarketplaceRegistryClient {
   }
   }
 
 
   private isCompatible(bundle: MarketplacePackageBundle): boolean {
   private isCompatible(bundle: MarketplacePackageBundle): boolean {
-    return (
-      satisfies(
-        this.options.pluginVersion,
-        bundle.manifest.compatibility.plugin,
-      ) &&
-      satisfies(
-        this.roleContractVersion,
-        bundle.manifest.compatibility.roleContract,
-      )
+    return satisfies(
+      this.options.pluginVersion,
+      bundle.manifest.compatibility.plugin,
     );
     );
   }
   }
 
 

+ 27 - 105
src/marketplace/schemas.test.ts

@@ -2,174 +2,96 @@ import { describe, expect, test } from 'bun:test';
 import {
 import {
   MarketplaceAgentManifestSchema,
   MarketplaceAgentManifestSchema,
   MarketplacePackageManifestSchema,
   MarketplacePackageManifestSchema,
-  MarketplaceProfileManifestSchema,
   MarketplaceVersionSchema,
   MarketplaceVersionSchema,
 } from './schemas';
 } from './schemas';
 
 
 const common = {
 const common = {
-  schemaVersion: 1 as const,
+  schemaVersion: 2 as const,
   id: 'community/example',
   id: 'community/example',
   version: '1.0.0',
   version: '1.0.0',
   displayName: 'Example',
   displayName: 'Example',
   description: 'An example package',
   description: 'An example package',
-  instructions: 'Follow these instructions.',
+  agentName: 'example',
+  prompt: 'Follow these instructions.',
   author: { name: 'Example Community' },
   author: { name: 'Example Community' },
   tags: ['example'],
   tags: ['example'],
   license: 'MIT',
   license: 'MIT',
-  compatibility: {
-    plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-    roleContract: '^1.0.0',
-  },
+  compatibility: { plugin: '>=3.0.0-beta.3 <4.0.0' },
   routing: {
   routing: {
     description: 'Explore example code.',
     description: 'Explore example code.',
+    when: 'When exploration is needed.',
     keywords: ['example'],
     keywords: ['example'],
-    delegation: { when: 'When exploration is needed.', preferredRoles: [] },
-  },
-  requirements: {
-    skills: { required: [], optional: [] },
-    mcps: { required: [], optional: [] },
   },
   },
-  capabilities: { tools: [], permissions: [] },
+  skills: [],
+  mcps: [],
+  tools: [],
+  model: { source: 'explicit' as const, candidates: ['provider/model'] },
 };
 };
 
 
-describe('marketplace manifest schemas', () => {
+describe('agents-only marketplace manifest schemas', () => {
   test('rejects non-canonical semantic-version aliases', () => {
   test('rejects non-canonical semantic-version aliases', () => {
     expect(MarketplaceVersionSchema.safeParse('v1.0.0').success).toBe(false);
     expect(MarketplaceVersionSchema.safeParse('v1.0.0').success).toBe(false);
     expect(
     expect(
       MarketplacePackageManifestSchema.safeParse({
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         ...common,
         version: 'v1.0.0',
         version: 'v1.0.0',
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
   });
   });
 
 
-  test('accept only data-only agent and profile manifests', () => {
+  test('accepts standalone and single-builtin extension agents', () => {
+    expect(MarketplaceAgentManifestSchema.safeParse(common).success).toBe(true);
     expect(
     expect(
       MarketplaceAgentManifestSchema.safeParse({
       MarketplaceAgentManifestSchema.safeParse({
         ...common,
         ...common,
-        kind: 'agent',
-        baseRole: 'fixer',
-        agentName: 'example',
-        overrides: {},
-      }).success,
-    ).toBe(true);
-    expect(
-      MarketplaceProfileManifestSchema.safeParse({
-        ...common,
-        kind: 'profile',
-        targetRole: 'oracle',
-        instructionMode: 'append',
-        overrides: {},
+        extends: { builtin: 'explorer', promptMode: 'append' },
+        model: { source: 'builtin' },
       }).success,
       }).success,
     ).toBe(true);
     ).toBe(true);
     expect(
     expect(
       MarketplacePackageManifestSchema.safeParse({
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         ...common,
-        kind: 'agent',
-        baseRole: 'orchestrator',
-        agentName: 'example',
-        overrides: {},
+        extends: {
+          builtin: 'explorer',
+          promptMode: 'append',
+          extra: true,
+        },
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
   });
   });
 
 
-  test('rejects executable or non-canonical manifest fields', () => {
+  test('rejects extension-only builtin model policy for standalone agents', () => {
     expect(
     expect(
-      MarketplacePackageManifestSchema.safeParse({
-        ...common,
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
-        scripts: { install: 'rm -rf /' },
-      }).success,
-    ).toBe(false);
-    expect(
-      MarketplacePackageManifestSchema.safeParse({
-        ...common,
-        id: 'Community/Example',
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
-      }).success,
-    ).toBe(false);
-    expect(
-      MarketplacePackageManifestSchema.safeParse({
-        ...common,
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
-        version: '^1.0.0',
-      }).success,
-    ).toBe(false);
-    expect(
-      MarketplacePackageManifestSchema.safeParse({
+      MarketplaceAgentManifestSchema.safeParse({
         ...common,
         ...common,
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: { promptSuffix: 'not canonical' },
+        model: { source: 'builtin' },
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
   });
   });
 
 
-  test('rejects duplicate capabilities', () => {
+  test('rejects executable, duplicate, and unknown manifest fields', () => {
     expect(
     expect(
       MarketplacePackageManifestSchema.safeParse({
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         ...common,
-        capabilities: { tools: ['read', 'read'], permissions: [] },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        scripts: { install: 'rm -rf /' },
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
     expect(
     expect(
       MarketplacePackageManifestSchema.safeParse({
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         ...common,
-        capabilities: {
-          tools: [],
-          permissions: ['network.fetch', 'network.fetch'],
-        },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        id: 'Community/Example',
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
-  });
-
-  test('rejects overlapping required and optional requirements', () => {
     expect(
     expect(
       MarketplacePackageManifestSchema.safeParse({
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         ...common,
-        requirements: {
-          skills: { required: ['code-search'], optional: ['code-search'] },
-          mcps: { required: [], optional: [] },
-        },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        tools: ['read', 'read'],
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
     expect(
     expect(
       MarketplacePackageManifestSchema.safeParse({
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         ...common,
-        requirements: {
-          skills: { required: [], optional: [] },
-          mcps: { required: ['crawl4ai'], optional: ['crawl4ai'] },
-        },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        unknownField: true,
       }).success,
       }).success,
     ).toBe(false);
     ).toBe(false);
   });
   });

+ 113 - 146
src/marketplace/schemas.ts

@@ -2,11 +2,11 @@ import { isAbsolute } from 'node:path';
 import { valid, validRange } from 'semver';
 import { valid, validRange } from 'semver';
 import { z } from 'zod';
 import { z } from 'zod';
 import { SUPPORTED_SPECIALIST_ROLES } from '../config/agent-roles';
 import { SUPPORTED_SPECIALIST_ROLES } from '../config/agent-roles';
+import { AGENT_THEME_COLORS } from '../config/constants';
 
 
-export const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 1 as const;
-export const MARKETPLACE_LOCKFILE_SCHEMA_VERSION = 1 as const;
-export const MARKETPLACE_ROLE_CONTRACT_VERSION = '1.0.0' as const;
-export const MARKETPLACE_DIGEST_DOMAIN = 'marketplace-bundle-v1' as const;
+export const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 2 as const;
+export const MARKETPLACE_LOCKFILE_SCHEMA_VERSION = 2 as const;
+export const MARKETPLACE_DIGEST_DOMAIN = 'marketplace-agent-bundle-v2' as const;
 
 
 const packageIdPattern =
 const packageIdPattern =
   /^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/;
   /^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/;
@@ -30,9 +30,32 @@ export const MarketplaceVersionSchema = z
     'Expected canonical exact semantic version spelling',
     'Expected canonical exact semantic version spelling',
   );
   );
 
 
-export const MarketplaceRoleSchema = z.enum(SUPPORTED_SPECIALIST_ROLES);
+export const MarketplaceBuiltinSchema = z.enum(SUPPORTED_SPECIALIST_ROLES);
+export type MarketplaceBuiltin = z.infer<typeof MarketplaceBuiltinSchema>;
 
 
 const BoundedTextSchema = (max: number) => z.string().trim().min(1).max(max);
 const BoundedTextSchema = (max: number) => z.string().trim().min(1).max(max);
+const UniqueStringArraySchema = z
+  .array(z.string().trim().min(1).max(200))
+  .max(128)
+  .refine((values) => new Set(values).size === values.length, {
+    message: 'Values must be unique',
+  });
+
+export const MARKETPLACE_TOOL_NAMES = [
+  'read',
+  'glob',
+  'grep',
+  'ast_grep_search',
+  'edit',
+  'write',
+  'apply_patch',
+  'ast_grep_replace',
+  'bash',
+  'webfetch',
+  'websearch',
+] as const;
+
+const MarketplaceToolSchema = z.enum(MARKETPLACE_TOOL_NAMES);
 
 
 export const MarketplaceAuthorSchema = z
 export const MarketplaceAuthorSchema = z
   .object({
   .object({
@@ -42,88 +65,29 @@ export const MarketplaceAuthorSchema = z
   })
   })
   .strict();
   .strict();
 
 
-const UniqueBoundedStringArraySchema: z.ZodType<string[]> = z
-  .array(z.string().trim().min(1).max(64))
-  .max(32)
-  .refine((values) => new Set(values).size === values.length, {
-    message: 'Values must be unique',
-  });
-
-function uniqueCapabilityArray<T extends z.ZodTypeAny>(
-  itemSchema: T,
-  max: number,
-) {
-  return z
-    .array(itemSchema)
-    .max(max)
-    .refine((values) => new Set(values).size === values.length, {
-      message: 'Values must be unique',
-    });
-}
+export const MarketplaceModelCandidateSchema = z.union([
+  BoundedTextSchema(200),
+  z
+    .object({
+      id: BoundedTextSchema(200),
+      variant: BoundedTextSchema(100).optional(),
+    })
+    .strict(),
+]);
 
 
-const RequirementSetSchema = z
+const MarketplaceExplicitModelSchema = z
   .object({
   .object({
-    required: UniqueBoundedStringArraySchema.default([]),
-    optional: UniqueBoundedStringArraySchema.default([]),
+    source: z.literal('explicit'),
+    candidates: z.array(MarketplaceModelCandidateSchema).min(1).max(16),
   })
   })
   .strict();
   .strict();
 
 
-export const MarketplaceRequirementsSchema = z
-  .object({
-    skills: RequirementSetSchema.default({ required: [], optional: [] }),
-    mcps: RequirementSetSchema.default({ required: [], optional: [] }),
-  })
-  .strict()
-  .superRefine((requirements, ctx) => {
-    if (
-      requirements.skills.required.some((skill) =>
-        requirements.skills.optional.includes(skill),
-      )
-    ) {
-      ctx.addIssue({
-        code: 'custom',
-        path: ['skills'],
-        message: 'Required and optional skills must not overlap',
-      });
-    }
-    if (
-      requirements.mcps.required.some((mcp) =>
-        requirements.mcps.optional.includes(mcp),
-      )
-    ) {
-      ctx.addIssue({
-        code: 'custom',
-        path: ['mcps'],
-        message: 'Required and optional MCPs must not overlap',
-      });
-    }
-  });
-
-export const MarketplaceCapabilityRequestSchema = z
-  .object({
-    tools: uniqueCapabilityArray(
-      z.enum([
-        'read',
-        'glob',
-        'grep',
-        'ast_grep_search',
-        'webfetch',
-        'websearch',
-      ]),
-      6,
-    ).default([]),
-    permissions: uniqueCapabilityArray(
-      z.enum([
-        'filesystem.read',
-        'filesystem.write',
-        'network.fetch',
-        'session.delegate',
-      ]),
-      4,
-    ).default([]),
-  })
-  .strict()
-  .default({ tools: [], permissions: [] });
+export const MarketplaceModelPolicySchema = z.discriminatedUnion('source', [
+  z.object({ source: z.literal('session') }).strict(),
+  z.object({ source: z.literal('orchestrator') }).strict(),
+  MarketplaceExplicitModelSchema,
+  z.object({ source: z.literal('builtin') }).strict(),
+]);
 
 
 export const MarketplaceCompatibilitySchema = z
 export const MarketplaceCompatibilitySchema = z
   .object({
   .object({
@@ -131,83 +95,87 @@ export const MarketplaceCompatibilitySchema = z
       (range) => validRange(range) !== null,
       (range) => validRange(range) !== null,
       'Expected a valid plugin semantic-version range',
       'Expected a valid plugin semantic-version range',
     ),
     ),
-    roleContract: BoundedTextSchema(64).refine(
-      (range) => validRange(range) !== null,
-      'Expected a valid role-contract semantic-version range',
-    ),
   })
   })
   .strict();
   .strict();
 
 
 export const MarketplaceRoutingSchema = z
 export const MarketplaceRoutingSchema = z
   .object({
   .object({
     description: BoundedTextSchema(500),
     description: BoundedTextSchema(500),
-    keywords: UniqueBoundedStringArraySchema.default([]),
-    delegation: z
-      .object({
-        when: BoundedTextSchema(500),
-        preferredRoles: z.array(MarketplaceRoleSchema).max(6).default([]),
-      })
-      .strict(),
+    when: BoundedTextSchema(500),
+    keywords: UniqueStringArraySchema,
   })
   })
   .strict();
   .strict();
 
 
-export const MarketplaceAgentOverridesSchema = z
+export const MarketplaceExtensionSchema = z
   .object({
   .object({
-    model: z.string().trim().min(1).max(200).optional(),
-    variant: z.string().trim().min(1).max(100).optional(),
-    temperature: z.number().min(0).max(2).optional(),
-    displayName: BoundedTextSchema(120).optional(),
-    description: BoundedTextSchema(500).optional(),
+    builtin: MarketplaceBuiltinSchema,
+    promptMode: z.enum(['append', 'replace']),
   })
   })
-  .strict()
-  .default({});
+  .strict();
 
 
-const ManifestBaseSchema = z.object({
-  schemaVersion: z
-    .literal(MARKETPLACE_MANIFEST_SCHEMA_VERSION)
-    .default(MARKETPLACE_MANIFEST_SCHEMA_VERSION),
-  id: MarketplacePackageIdSchema,
-  version: MarketplaceVersionSchema,
-  displayName: BoundedTextSchema(120),
-  description: BoundedTextSchema(1000),
-  instructions: BoundedTextSchema(100_000),
-  author: MarketplaceAuthorSchema,
-  tags: UniqueBoundedStringArraySchema,
-  license: BoundedTextSchema(64),
-  compatibility: MarketplaceCompatibilitySchema,
-  routing: MarketplaceRoutingSchema,
-  requirements: MarketplaceRequirementsSchema,
-  capabilities: MarketplaceCapabilityRequestSchema,
-});
+const ManifestSchema = z
+  .object({
+    schemaVersion: z.literal(MARKETPLACE_MANIFEST_SCHEMA_VERSION),
+    id: MarketplacePackageIdSchema,
+    version: MarketplaceVersionSchema,
+    displayName: BoundedTextSchema(120),
+    description: BoundedTextSchema(1000),
+    agentName: z
+      .string()
+      .trim()
+      .regex(/^[a-z][a-z0-9_-]{0,63}$/, 'Expected a valid agent name'),
+    prompt: BoundedTextSchema(100_000),
+    routing: MarketplaceRoutingSchema,
+    skills: UniqueStringArraySchema,
+    mcps: UniqueStringArraySchema,
+    tools: z
+      .array(MarketplaceToolSchema)
+      .max(11)
+      .refine((values) => new Set(values).size === values.length, {
+        message: 'Tools must be unique',
+      }),
+    author: MarketplaceAuthorSchema,
+    tags: UniqueStringArraySchema,
+    license: BoundedTextSchema(64),
+    compatibility: MarketplaceCompatibilitySchema,
+    model: MarketplaceModelPolicySchema,
+    temperature: z.number().min(0).max(2).optional(),
+    color: z
+      .union([
+        z.string().regex(/^#[0-9a-fA-F]{6}$/),
+        z.enum(AGENT_THEME_COLORS),
+      ])
+      .optional(),
+  })
+  .strict();
 
 
-export const MarketplaceAgentManifestSchema = ManifestBaseSchema.extend({
-  kind: z.literal('agent'),
-  baseRole: MarketplaceRoleSchema,
-  agentName: z
-    .string()
-    .trim()
-    .regex(/^[a-z][a-z0-9_-]{0,63}$/, 'Expected a valid agent name'),
-  overrides: MarketplaceAgentOverridesSchema,
-}).strict();
+export const MarketplacePackageManifestSchema = ManifestSchema.extend({
+  extends: MarketplaceExtensionSchema.optional(),
+})
+  .strict()
+  .superRefine((manifest, ctx) => {
+    if (manifest.model.source === 'builtin' && !manifest.extends) {
+      ctx.addIssue({
+        code: 'custom',
+        path: ['model', 'source'],
+        message: "model.source 'builtin' requires extends",
+      });
+    }
+  });
 
 
-export const MarketplaceProfileManifestSchema = ManifestBaseSchema.extend({
-  kind: z.literal('profile'),
-  targetRole: MarketplaceRoleSchema,
-  instructionMode: z.enum(['append', 'replace']),
-  overrides: MarketplaceAgentOverridesSchema,
-}).strict();
+export const MarketplaceAgentManifestSummarySchema = ManifestSchema.omit({
+  prompt: true,
+})
+  .extend({ extends: MarketplaceExtensionSchema.optional() })
+  .strict();
 
 
-export const MarketplacePackageManifestSchema = z.discriminatedUnion('kind', [
-  MarketplaceAgentManifestSchema,
-  MarketplaceProfileManifestSchema,
-]);
+// Kept as the precise agent-manifest name for consumers of the public contract.
+export const MarketplaceAgentManifestSchema = MarketplacePackageManifestSchema;
 
 
-/**
- * A package bundle deliberately contains only its manifest. There is no
- * executable entrypoint, hook, script, or arbitrary file payload in V3.
- */
 export const MarketplacePackageBundleSchema = z
 export const MarketplacePackageBundleSchema = z
-  .object({ manifest: MarketplacePackageManifestSchema })
+  .object({
+    manifest: MarketplacePackageManifestSchema,
+  })
   .strict();
   .strict();
 
 
 export const MarketplaceLocalSourceSchema = z
 export const MarketplaceLocalSourceSchema = z
@@ -269,13 +237,12 @@ export const MarketplaceLockfileSchema = z
 
 
 export type MarketplacePackageId = z.infer<typeof MarketplacePackageIdSchema>;
 export type MarketplacePackageId = z.infer<typeof MarketplacePackageIdSchema>;
 export type MarketplaceVersion = z.infer<typeof MarketplaceVersionSchema>;
 export type MarketplaceVersion = z.infer<typeof MarketplaceVersionSchema>;
-export type MarketplaceRole = z.infer<typeof MarketplaceRoleSchema>;
+export type MarketplaceModelPolicy = z.infer<
+  typeof MarketplaceModelPolicySchema
+>;
 export type MarketplaceAgentManifest = z.infer<
 export type MarketplaceAgentManifest = z.infer<
   typeof MarketplaceAgentManifestSchema
   typeof MarketplaceAgentManifestSchema
 >;
 >;
-export type MarketplaceProfileManifest = z.infer<
-  typeof MarketplaceProfileManifestSchema
->;
 export type MarketplacePackageManifest = z.infer<
 export type MarketplacePackageManifest = z.infer<
   typeof MarketplacePackageManifestSchema
   typeof MarketplacePackageManifestSchema
 >;
 >;

+ 0 - 1
src/marketplace/service.ts

@@ -83,7 +83,6 @@ export class MarketplaceService {
       new MarketplaceRegistryClient({
       new MarketplaceRegistryClient({
         pluginVersion:
         pluginVersion:
           options.pluginVersion ?? readPluginPackageVersion() ?? '0.0.0',
           options.pluginVersion ?? readPluginPackageVersion() ?? '0.0.0',
-        roleContractVersion: options.roleContractVersion,
       });
       });
   }
   }
 
 

+ 15 - 30
src/marketplace/status.test.ts

@@ -27,33 +27,28 @@ function agentBundle(
 ): MarketplacePackageBundle {
 ): MarketplacePackageBundle {
   return {
   return {
     manifest: {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id,
       id,
       version,
       version,
-      kind: 'agent',
       displayName: 'Docs researcher',
       displayName: 'Docs researcher',
       description: 'A derived explorer.',
       description: 'A derived explorer.',
-      instructions: 'Prefer documentation paths first.',
+      agentName: id.split('/')[1]?.replaceAll('-', '') ?? 'docsresearcher',
+      prompt: 'Prefer documentation paths first.',
       author: { name: 'Community' },
       author: { name: 'Community' },
       tags: ['docs'],
       tags: ['docs'],
       license: 'MIT',
       license: 'MIT',
       compatibility: {
       compatibility: {
-        plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-        roleContract: '^1.0.0',
+        plugin: '>=3.0.0-beta.3 <4.0.0',
       },
       },
       routing: {
       routing: {
         description: 'Research docs.',
         description: 'Research docs.',
         keywords: ['docs'],
         keywords: ['docs'],
-        delegation: { when: 'When needed.', preferredRoles: [] },
-      },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
+        when: 'When needed.',
       },
       },
-      capabilities: { tools: [], permissions: [] },
-      baseRole: 'explorer',
-      agentName: id.split('/')[1]?.replaceAll('-', '') ?? 'docsresearcher',
-      overrides: {},
+      skills: [],
+      mcps: [],
+      tools: [],
+      model: { source: 'explicit', candidates: ['provider/model'] },
       ...overrides,
       ...overrides,
     } as MarketplacePackageBundle['manifest'],
     } as MarketplacePackageBundle['manifest'],
   };
   };
@@ -217,11 +212,7 @@ describe('marketplace status', () => {
   test('compares desired registry identities so aliases match and collisions are excluded', () => {
   test('compares desired registry identities so aliases match and collisions are excluded', () => {
     const { root, project, service } = setup();
     const { root, project, service } = setup();
     try {
     try {
-      service.install(
-        agentBundle('community/docs-researcher', '1.0.0', {
-          overrides: { displayName: 'docsalias' },
-        }),
-      );
+      service.install(agentBundle('community/docs-researcher', '1.0.0'));
       service.install(
       service.install(
         agentBundle('community/reserved-explorer', '1.0.0', {
         agentBundle('community/reserved-explorer', '1.0.0', {
           agentName: 'explorer',
           agentName: 'explorer',
@@ -234,7 +225,7 @@ describe('marketplace status', () => {
           preset: 'work',
           preset: 'work',
           presets: {
           presets: {
             work: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'docsalias' } },
               marketplace: {
               marketplace: {
                 agents: [
                 agents: [
                   'community/docs-researcher',
                   'community/docs-researcher',
@@ -297,19 +288,13 @@ describe('marketplace status', () => {
   test('keeps desired rejection diagnostics in status', () => {
   test('keeps desired rejection diagnostics in status', () => {
     const { root, project, service } = setup();
     const { root, project, service } = setup();
     try {
     try {
-      service.install(
-        agentBundle('community/docs-researcher', '1.0.0', {
-          overrides: { displayName: 'Docs Researcher' },
-        }),
-      );
+      service.install(agentBundle('community/docs-researcher', '1.0.0'));
       service.install(
       service.install(
         agentBundle('community/needs-skill', '1.0.0', {
         agentBundle('community/needs-skill', '1.0.0', {
           agentName: 'needsskill',
           agentName: 'needsskill',
           displayName: 'Needs skill',
           displayName: 'Needs skill',
-          requirements: {
-            skills: { required: ['not-a-real-skill'], optional: [] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['not-a-real-skill'],
+          mcps: [],
         }),
         }),
       );
       );
       writeFileSync(
       writeFileSync(
@@ -318,7 +303,7 @@ describe('marketplace status', () => {
           preset: 'work',
           preset: 'work',
           presets: {
           presets: {
             work: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'Docs Researcher' } },
               marketplace: {
               marketplace: {
                 agents: ['community/docs-researcher', 'community/needs-skill'],
                 agents: ['community/docs-researcher', 'community/needs-skill'],
               },
               },

+ 3 - 30
src/marketplace/status.ts

@@ -19,7 +19,6 @@ export interface MarketplaceLivePackage {
   version: string;
   version: string;
   digest: string;
   digest: string;
   runtimeName: string;
   runtimeName: string;
-  kind: 'agent' | 'profile';
 }
 }
 
 
 export interface MarketplaceLiveSnapshot {
 export interface MarketplaceLiveSnapshot {
@@ -30,9 +29,7 @@ export interface MarketplaceLiveSnapshot {
 export interface MarketplaceInstalledSummary {
 export interface MarketplaceInstalledSummary {
   id: string;
   id: string;
   version?: string;
   version?: string;
-  kind?: 'agent' | 'profile';
   displayName?: string;
   displayName?: string;
-  role?: string;
   digest?: string;
   digest?: string;
   valid: boolean;
   valid: boolean;
 }
 }
@@ -46,7 +43,6 @@ export interface MarketplaceStatusReport {
   configured: {
   configured: {
     preset?: string;
     preset?: string;
     agents: string[];
     agents: string[];
-    profiles: Record<string, string | null>;
   };
   };
   live?: {
   live?: {
     packages: MarketplaceLivePackage[];
     packages: MarketplaceLivePackage[];
@@ -64,19 +60,11 @@ export interface CollectMarketplaceStatusOptions {
   desiredLive?: MarketplaceLiveSnapshot;
   desiredLive?: MarketplaceLiveSnapshot;
 }
 }
 
 
-function packageRole(pkg: StoredMarketplacePackage): string {
-  return pkg.manifest.kind === 'agent'
-    ? pkg.manifest.baseRole
-    : pkg.manifest.targetRole;
-}
-
 function summarize(pkg: StoredMarketplacePackage): MarketplaceInstalledSummary {
 function summarize(pkg: StoredMarketplacePackage): MarketplaceInstalledSummary {
   return {
   return {
     id: pkg.manifest.id,
     id: pkg.manifest.id,
     version: pkg.manifest.version,
     version: pkg.manifest.version,
-    kind: pkg.manifest.kind,
     displayName: pkg.manifest.displayName,
     displayName: pkg.manifest.displayName,
-    role: packageRole(pkg),
     digest: pkg.digest,
     digest: pkg.digest,
     valid: true,
     valid: true,
   };
   };
@@ -86,10 +74,7 @@ function configuredIds(
   activation: MarketplaceActivation | undefined,
   activation: MarketplaceActivation | undefined,
 ): string[] {
 ): string[] {
   const agents = activation?.agents ?? [];
   const agents = activation?.agents ?? [];
-  const profiles = Object.values(activation?.profiles ?? {}).filter(
-    (value): value is string => typeof value === 'string' && value.length > 0,
-  );
-  return [...new Set([...agents, ...profiles])];
+  return [...new Set(agents)];
 }
 }
 
 
 function identityKey(entry: {
 function identityKey(entry: {
@@ -274,7 +259,6 @@ export function collectMarketplaceStatus(
     configured: {
     configured: {
       ...(preset ? { preset } : {}),
       ...(preset ? { preset } : {}),
       agents: [...(activation?.agents ?? [])],
       agents: [...(activation?.agents ?? [])],
-      profiles: { ...(activation?.profiles ?? {}) },
     },
     },
     ...(livePackages
     ...(livePackages
       ? {
       ? {
@@ -304,11 +288,9 @@ export function formatMarketplaceStatus(
   } else {
   } else {
     for (const pkg of report.installed) {
     for (const pkg of report.installed) {
       const version = pkg.version ? `@${pkg.version}` : '';
       const version = pkg.version ? `@${pkg.version}` : '';
-      const kind = pkg.kind ? ` ${pkg.kind}` : '';
-      const role = pkg.role ? ` ${pkg.role}` : '';
       const name = pkg.displayName ? ` ${pkg.displayName}` : '';
       const name = pkg.displayName ? ` ${pkg.displayName}` : '';
       const valid = pkg.valid ? '' : ' corrupt';
       const valid = pkg.valid ? '' : ' corrupt';
-      lines.push(`  ${pkg.id}${version}${kind}${role}${name}${valid}`);
+      lines.push(`  ${pkg.id}${version}${name}${valid}`);
     }
     }
   }
   }
   lines.push('', 'configured_agents:');
   lines.push('', 'configured_agents:');
@@ -317,15 +299,6 @@ export function formatMarketplaceStatus(
   } else {
   } else {
     for (const id of report.configured.agents) lines.push(`  ${id}`);
     for (const id of report.configured.agents) lines.push(`  ${id}`);
   }
   }
-  lines.push('', 'configured_profiles:');
-  const profileEntries = Object.entries(report.configured.profiles);
-  if (profileEntries.length === 0) {
-    lines.push('  (none)');
-  } else {
-    for (const [role, packageId] of profileEntries) {
-      lines.push(`  ${role}: ${packageId ?? '(cleared)'}`);
-    }
-  }
   if (report.live) {
   if (report.live) {
     lines.push('', 'live_packages:');
     lines.push('', 'live_packages:');
     if (report.live.packages.length === 0) {
     if (report.live.packages.length === 0) {
@@ -333,7 +306,7 @@ export function formatMarketplaceStatus(
     } else {
     } else {
       for (const entry of report.live.packages) {
       for (const entry of report.live.packages) {
         lines.push(
         lines.push(
-          `  ${entry.packageId}@${entry.version} ${entry.kind} ${entry.runtimeName}`,
+          `  ${entry.packageId}@${entry.version} ${entry.runtimeName}`,
         );
         );
       }
       }
     }
     }

+ 64 - 73
src/marketplace/store.test.ts

@@ -5,6 +5,7 @@ import {
   existsSync,
   existsSync,
   mkdirSync,
   mkdirSync,
   mkdtempSync,
   mkdtempSync,
+  readdirSync,
   readFileSync,
   readFileSync,
   rmSync,
   rmSync,
   writeFileSync,
   writeFileSync,
@@ -17,13 +18,11 @@ import {
   MarketplaceConflictError,
   MarketplaceConflictError,
   MarketplaceIntegrityError,
   MarketplaceIntegrityError,
   MarketplaceLockfileError,
   MarketplaceLockfileError,
-  MarketplaceLockOwnershipError,
   type MarketplacePackageBundle,
   type MarketplacePackageBundle,
   MarketplaceRetiredError,
   MarketplaceRetiredError,
   MarketplaceService,
   MarketplaceService,
   MarketplaceStore,
   MarketplaceStore,
 } from './index';
 } from './index';
-import { heartbeatPath, publishGeneration, readLeaseState } from './lease';
 import type { MarketplacePaths } from './paths';
 import type { MarketplacePaths } from './paths';
 
 
 function bundle(
 function bundle(
@@ -32,36 +31,28 @@ function bundle(
 ): MarketplacePackageBundle {
 ): MarketplacePackageBundle {
   return {
   return {
     manifest: {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/example',
       id: 'community/example',
       version,
       version,
-      kind: 'agent',
       displayName: 'Example',
       displayName: 'Example',
       description: 'An example package',
       description: 'An example package',
-      instructions: 'Use the example role carefully.',
+      agentName: 'example',
+      prompt: 'Use the example role carefully.',
       author: { name: 'Example Community' },
       author: { name: 'Example Community' },
       tags: ['example'],
       tags: ['example'],
       license: 'MIT',
       license: 'MIT',
       compatibility: {
       compatibility: {
-        plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-        roleContract: '^1.0.0',
+        plugin: '>=3.0.0-beta.3 <4.0.0',
       },
       },
       routing: {
       routing: {
         description: 'Explore example code.',
         description: 'Explore example code.',
         keywords: ['example'],
         keywords: ['example'],
-        delegation: {
-          when: 'When repository exploration is needed.',
-          preferredRoles: [],
-        },
+        when: 'When repository exploration is needed.',
       },
       },
-      baseRole: 'explorer',
-      agentName: 'example',
-      overrides: {},
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
-      },
-      capabilities: { tools: [], permissions: [] },
+      skills: [],
+      mcps: [],
+      tools: [],
+      model: { source: 'explicit', candidates: ['provider/model'] },
       ...overrides,
       ...overrides,
     } as MarketplacePackageBundle['manifest'],
     } as MarketplacePackageBundle['manifest'],
   };
   };
@@ -71,18 +62,13 @@ function tempRoot(): string {
   return mkdtempSync(join(tmpdir(), 'marketplace-store-'));
   return mkdtempSync(join(tmpdir(), 'marketplace-store-'));
 }
 }
 
 
-function markStaleGeneration(directory: string, generation: string): void {
-  const target = join(directory, 'gen', generation);
-  mkdirSync(target, { recursive: true });
-  writeFileSync(
-    join(target, 'meta.json'),
-    JSON.stringify({ generation, token: 'stale-token', pid: -1 }),
-  );
-  writeFileSync(join(target, 'heartbeat'), '');
+function markStaleLease(directory: string, pid: number, uuid: string): void {
+  mkdirSync(directory, { recursive: true });
+  const target = join(directory, `${pid}.${uuid}.lease`);
+  const fd = fsModule.openSync(target, 'wx', 0o600);
+  fsModule.closeSync(fd);
   const old = new Date(Date.now() - 2_000);
   const old = new Date(Date.now() - 2_000);
   fsModule.utimesSync(target, old, old);
   fsModule.utimesSync(target, old, old);
-  fsModule.utimesSync(join(target, 'meta.json'), old, old);
-  fsModule.utimesSync(join(target, 'heartbeat'), old, old);
 }
 }
 
 
 function spawnLockWorker(
 function spawnLockWorker(
@@ -98,7 +84,7 @@ function spawnLockWorker(
 ${body}`,
 ${body}`,
       JSON.stringify({ paths, logPath }),
       JSON.stringify({ paths, logPath }),
     ],
     ],
-    { stdout: 'ignore', stderr: 'ignore' },
+    { stdout: 'ignore', stderr: 'pipe' },
   );
   );
 }
 }
 
 
@@ -131,12 +117,12 @@ describe('MarketplaceStore', () => {
         'community/example',
         'community/example',
       ]);
       ]);
       expect(store.getLockfile().packages['community/example']).toEqual({
       expect(store.getLockfile().packages['community/example']).toEqual({
-        manifestSchemaVersion: 1,
+        manifestSchemaVersion: 2,
         manifestVersion: '1.0.0',
         manifestVersion: '1.0.0',
         source: { kind: 'in-memory', label: 'programmatic' },
         source: { kind: 'in-memory', label: 'programmatic' },
         digest: {
         digest: {
           algorithm: 'sha256',
           algorithm: 'sha256',
-          domain: 'marketplace-bundle-v1',
+          domain: 'marketplace-agent-bundle-v2',
           value: installed.digest,
           value: installed.digest,
         },
         },
       });
       });
@@ -250,9 +236,7 @@ describe('MarketplaceStore', () => {
       const store = new MarketplaceStore({ rootDir: root });
       const store = new MarketplaceStore({ rootDir: root });
       store.install(bundle());
       store.install(bundle());
       expect(() =>
       expect(() =>
-        store.install(
-          bundle('1.0.0', { instructions: 'Changed instructions.' }),
-        ),
+        store.install(bundle('1.0.0', { prompt: 'Changed instructions.' })),
       ).toThrow(MarketplaceConflictError);
       ).toThrow(MarketplaceConflictError);
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
@@ -327,12 +311,11 @@ describe('MarketplaceStore', () => {
         lock: { timeoutMs: 10, retryMs: 1 },
         lock: { timeoutMs: 10, retryMs: 1 },
       });
       });
       mkdirSync(root, { recursive: true });
       mkdirSync(root, { recursive: true });
-      expect(
-        publishGeneration(store.paths.lockDir, {
-          generation: 'blocking-generation',
-          token: 'blocking-token',
-        }),
-      ).toBe(true);
+      markStaleLease(
+        store.paths.lockDir,
+        process.pid,
+        '00000000-0000-4000-8000-000000000001',
+      );
       expect(() => store.install(bundle())).toThrow(MarketplaceBusyError);
       expect(() => store.install(bundle())).toThrow(MarketplaceBusyError);
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
@@ -344,10 +327,14 @@ describe('MarketplaceStore', () => {
     try {
     try {
       const store = new MarketplaceStore({
       const store = new MarketplaceStore({
         rootDir: root,
         rootDir: root,
-        lock: { staleMs: 200, heartbeatMs: 50, retryMs: 1 },
+        lock: { staleMs: 200, retryMs: 1 },
       });
       });
       mkdirSync(root, { recursive: true });
       mkdirSync(root, { recursive: true });
-      markStaleGeneration(store.paths.lockDir, 'dead-lock-generation');
+      markStaleLease(
+        store.paths.lockDir,
+        999_999_999,
+        '00000000-0000-4000-8000-000000000001',
+      );
       expect(store.install(bundle()).manifest.id).toBe('community/example');
       expect(store.install(bundle()).manifest.id).toBe('community/example');
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
@@ -359,7 +346,7 @@ describe('MarketplaceStore', () => {
     const readyPath = join(root, 'worker-ready');
     const readyPath = join(root, 'worker-ready');
     const store = new MarketplaceStore({
     const store = new MarketplaceStore({
       rootDir: root,
       rootDir: root,
-      lock: { staleMs: 100, heartbeatMs: 10, timeoutMs: 30, retryMs: 1 },
+      lock: { staleMs: 500, timeoutMs: 30, retryMs: 1 },
     });
     });
     const worker = Bun.spawn(
     const worker = Bun.spawn(
       [
       [
@@ -368,7 +355,7 @@ describe('MarketplaceStore', () => {
         `import { writeFileSync } from 'node:fs';
         `import { writeFileSync } from 'node:fs';
 import { acquireMarketplaceLeaseForPaths } from './src/marketplace/store.ts';
 import { acquireMarketplaceLeaseForPaths } from './src/marketplace/store.ts';
 const { paths, readyPath } = JSON.parse(process.argv[1]);
 const { paths, readyPath } = JSON.parse(process.argv[1]);
-acquireMarketplaceLeaseForPaths(paths, { staleMs: 100, timeoutMs: 5000, heartbeatMs: 10 });
+acquireMarketplaceLeaseForPaths(paths, { staleMs: 500, timeoutMs: 5000 });
 writeFileSync(readyPath, 'ready');
 writeFileSync(readyPath, 'ready');
 await new Promise(() => {});`,
 await new Promise(() => {});`,
         JSON.stringify({ paths: store.paths, readyPath }),
         JSON.stringify({ paths: store.paths, readyPath }),
@@ -387,7 +374,7 @@ await new Promise(() => {});`,
       expect(() => store.list()).toThrow(MarketplaceBusyError);
       expect(() => store.list()).toThrow(MarketplaceBusyError);
       worker.kill();
       worker.kill();
       await worker.exited;
       await worker.exited;
-      await Bun.sleep(250);
+      await Bun.sleep(700);
       expect(store.list()).toEqual([]);
       expect(store.list()).toEqual([]);
     } finally {
     } finally {
       worker.kill();
       worker.kill();
@@ -400,15 +387,18 @@ await new Promise(() => {});`,
     const root = tempRoot();
     const root = tempRoot();
     const store = new MarketplaceStore({
     const store = new MarketplaceStore({
       rootDir: root,
       rootDir: root,
-      lock: { staleMs: 1000, heartbeatMs: 100, timeoutMs: 2000, retryMs: 5 },
+      lock: { staleMs: 1000, timeoutMs: 2000, retryMs: 5 },
     });
     });
     const logPath = join(root, 'reclaim.log');
     const logPath = join(root, 'reclaim.log');
     try {
     try {
-      markStaleGeneration(store.paths.lockDir, 'stale-lock-generation');
-      markStaleGeneration(store.paths.breakerDir, 'stale-breaker-generation');
+      markStaleLease(
+        store.paths.lockDir,
+        999_999_999,
+        '00000000-0000-4000-8000-000000000001',
+      );
       const body = `import { appendFileSync, closeSync, openSync, rmSync } from 'node:fs';
       const body = `import { appendFileSync, closeSync, openSync, rmSync } from 'node:fs';
 const { paths, logPath } = JSON.parse(process.argv[1]);
 const { paths, logPath } = JSON.parse(process.argv[1]);
-const lease = acquireMarketplaceLeaseForPaths(paths, { staleMs: 1000, heartbeatMs: 100, timeoutMs: 2000, retryMs: 5 });
+const lease = acquireMarketplaceLeaseForPaths(paths, { staleMs: 1000, timeoutMs: 2000, retryMs: 5 });
 const activePath = logPath + '.active';
 const activePath = logPath + '.active';
 try {
 try {
   lease.commit(() => {
   lease.commit(() => {
@@ -429,6 +419,12 @@ try {
       const exitCodes = await Promise.all(
       const exitCodes = await Promise.all(
         workers.map((worker) => worker.exited),
         workers.map((worker) => worker.exited),
       );
       );
+      const errors = await Promise.all(
+        workers.map((worker) => new Response(worker.stderr).text()),
+      );
+      if (exitCodes.some((code) => code !== 0)) {
+        throw new Error(errors.join('\n'));
+      }
       expect(exitCodes).toEqual([0, 0]);
       expect(exitCodes).toEqual([0, 0]);
       expect(readFileSync(logPath, 'utf8').trim().split('\n')).toEqual([
       expect(readFileSync(logPath, 'utf8').trim().split('\n')).toEqual([
         'acquired',
         'acquired',
@@ -436,44 +432,39 @@ try {
         'acquired',
         'acquired',
         'released',
         'released',
       ]);
       ]);
-      expect(readLeaseState(store.paths.breakerDir)).toBeUndefined();
+      expect(existsSync(store.paths.lockDir)).toBe(true);
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
   });
   });
 
 
-  test('fences an expired owner when a replacement generation takes over', () => {
+  test('keeps a live owner authoritative after its lease becomes old', () => {
     const root = tempRoot();
     const root = tempRoot();
     const store = new MarketplaceStore({
     const store = new MarketplaceStore({
       rootDir: root,
       rootDir: root,
-      lock: { staleMs: 200, heartbeatMs: 50, timeoutMs: 1000, retryMs: 10 },
+      lock: { staleMs: 1_000, timeoutMs: 1000, retryMs: 10 },
     });
     });
     try {
     try {
       const owner = acquireMarketplaceLeaseForPaths(store.paths, {
       const owner = acquireMarketplaceLeaseForPaths(store.paths, {
-        staleMs: 200,
-        heartbeatMs: 50,
+        staleMs: 1_000,
         timeoutMs: 1000,
         timeoutMs: 1000,
         retryMs: 10,
         retryMs: 10,
       });
       });
       const old = new Date(Date.now() - 2_000);
       const old = new Date(Date.now() - 2_000);
-      fsModule.utimesSync(
-        heartbeatPath(store.paths.lockDir, owner.generation),
-        old,
-        old,
-      );
-      const replacement = acquireMarketplaceLeaseForPaths(store.paths, {
-        staleMs: 200,
-        heartbeatMs: 50,
-        timeoutMs: 1000,
-        retryMs: 10,
-      });
-      expect(() => owner.release()).toThrow(MarketplaceLockOwnershipError);
-      expect(() => owner.commit(() => undefined)).toThrow(
-        MarketplaceLockOwnershipError,
+      const leasePath = join(
+        store.paths.lockDir,
+        readdirSync(store.paths.lockDir)[0],
       );
       );
-      expect(() => replacement.assertCurrent()).not.toThrow();
-      expect(() => replacement.commit(() => undefined)).not.toThrow();
-      replacement.release();
+      fsModule.utimesSync(leasePath, old, old);
+      expect(() =>
+        acquireMarketplaceLeaseForPaths(store.paths, {
+          staleMs: 1_000,
+          timeoutMs: 120,
+          retryMs: 10,
+        }),
+      ).toThrow(MarketplaceBusyError);
+      expect(() => owner.assertCurrent()).not.toThrow();
+      owner.release();
     } finally {
     } finally {
       rmSync(root, { recursive: true, force: true });
       rmSync(root, { recursive: true, force: true });
     }
     }
@@ -631,7 +622,7 @@ describe('MarketplaceService', () => {
 
 
       expect(() =>
       expect(() =>
         service.install(
         service.install(
-          bundle('1.0.0', { instructions: 'A different package body.' }),
+          bundle('1.0.0', { prompt: 'A different package body.' }),
         ),
         ),
       ).toThrow(MarketplaceConflictError);
       ).toThrow(MarketplaceConflictError);
 
 

+ 0 - 1
src/marketplace/store.ts

@@ -45,7 +45,6 @@ export type { MarketplaceLease, MarketplaceLockOptions };
 export interface MarketplaceStoreOptions {
 export interface MarketplaceStoreOptions {
   rootDir?: string;
   rootDir?: string;
   pluginVersion?: string;
   pluginVersion?: string;
-  roleContractVersion?: string;
   lock?: Partial<MarketplaceLockOptions>;
   lock?: Partial<MarketplaceLockOptions>;
 }
 }
 
 

+ 119 - 219
src/tools/marketplace.test.ts

@@ -33,78 +33,33 @@ function agentBundle(
 ): MarketplacePackageBundle {
 ): MarketplacePackageBundle {
   return {
   return {
     manifest: {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/docs-researcher',
       id: 'community/docs-researcher',
       version,
       version,
-      kind: 'agent',
       displayName: 'Docs researcher',
       displayName: 'Docs researcher',
       description: 'A derived explorer.',
       description: 'A derived explorer.',
-      instructions: 'Prefer documentation paths first.',
+      agentName: 'docsresearcher',
+      prompt: 'Prefer documentation paths first.',
       author: { name: 'Community' },
       author: { name: 'Community' },
       tags: ['docs'],
       tags: ['docs'],
       license: 'MIT',
       license: 'MIT',
       compatibility: {
       compatibility: {
-        plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-        roleContract: '^1.0.0',
+        plugin: '>=3.0.0-beta.3 <4.0.0',
       },
       },
       routing: {
       routing: {
         description: 'Research docs and examples.',
         description: 'Research docs and examples.',
         keywords: ['docs'],
         keywords: ['docs'],
-        delegation: {
-          when: 'When docs research is needed.',
-          preferredRoles: [],
-        },
+        when: 'When docs research is needed.',
       },
       },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
-      },
-      capabilities: { tools: [], permissions: [] },
-      baseRole: 'explorer',
-      agentName: 'docsresearcher',
-      overrides: {},
+      skills: [],
+      mcps: [],
+      tools: [],
+      model: { source: 'explicit', candidates: ['provider/model'] },
       ...overrides,
       ...overrides,
     } as MarketplacePackageBundle['manifest'],
     } as MarketplacePackageBundle['manifest'],
   };
   };
 }
 }
 
 
-function profileBundle(): MarketplacePackageBundle {
-  return {
-    manifest: {
-      schemaVersion: 1,
-      id: 'community/deep-explorer',
-      version: '1.0.0',
-      kind: 'profile',
-      displayName: 'Deep explorer',
-      description: 'A specialist profile.',
-      instructions: 'Search more exhaustively than usual.',
-      author: { name: 'Community' },
-      tags: ['profile'],
-      license: 'MIT',
-      compatibility: {
-        plugin: '>=2.2.0 <3.0.0 || >=3.0.0-beta.0 <4.0.0',
-        roleContract: '^1.0.0',
-      },
-      routing: {
-        description: 'Deep exploration profile.',
-        keywords: ['deep'],
-        delegation: {
-          when: 'When exhaustive search is needed.',
-          preferredRoles: [],
-        },
-      },
-      requirements: {
-        skills: { required: [], optional: [] },
-        mcps: { required: [], optional: [] },
-      },
-      capabilities: { tools: [], permissions: [] },
-      targetRole: 'explorer',
-      instructionMode: 'append',
-      overrides: {},
-    } as MarketplacePackageBundle['manifest'],
-  };
-}
-
 function writeBundle(
 function writeBundle(
   directory: string,
   directory: string,
   bundle: MarketplacePackageBundle,
   bundle: MarketplacePackageBundle,
@@ -163,184 +118,132 @@ describe('marketplace tool args', () => {
     expect(() =>
     expect(() =>
       parseMarketplaceToolArgs({ action: 'list', path: './package.json' }),
       parseMarketplaceToolArgs({ action: 'list', path: './package.json' }),
     ).toThrow();
     ).toThrow();
-    expect(() =>
-      parseMarketplaceToolArgs({
-        action: 'profile',
-        role: 'explorer',
-        packageId: 'community/deep-explorer',
-        clear: true,
-      }),
-    ).toThrow();
   });
   });
 });
 });
 
 
 describe('marketplace tool', () => {
 describe('marketplace tool', () => {
-  test('covers local lifecycle, status, reload notice, and no live-registry mutation', async () => {
-    const { root, project, configHome, marketplace, context, live } =
-      setupHarness();
-    try {
-      const agentPath = writeBundle(project, agentBundle());
-      const profilePath = writeBundle(project, profileBundle(), 'profile.json');
-      const v2Path = writeBundle(
-        project,
-        agentBundle('1.1.0'),
-        'package-v2.json',
-      );
+  test(
+    'covers local lifecycle, status, reload notice, and no live-registry mutation',
+    async () => {
+      const { root, project, configHome, marketplace, context, live } =
+        setupHarness();
+      try {
+        const agentPath = writeBundle(project, agentBundle());
+        const v2Path = writeBundle(
+          project,
+          agentBundle('1.1.0'),
+          'package-v2.json',
+        );
 
 
-      const installed = String(
-        await marketplace.execute(
-          { action: 'import', path: agentPath },
-          context,
-        ),
-      );
-      expect(installed).toContain('Imported community/docs-researcher@1.0.0');
-      expect(installed).toContain('reload_required: false');
-      expect(installed).not.toContain(MARKETPLACE_RELOAD_NOTICE);
+        const installed = String(
+          await marketplace.execute(
+            { action: 'import', path: agentPath },
+            context,
+          ),
+        );
+        expect(installed).toContain('Imported community/docs-researcher@1.0.0');
+        expect(installed).toContain('reload_required: false');
+        expect(installed).not.toContain(MARKETPLACE_RELOAD_NOTICE);
 
 
-      const imported = String(
-        await marketplace.execute(
-          { action: 'import', path: profilePath },
-          context,
-        ),
-      );
-      expect(imported).toContain('Imported community/deep-explorer@1.0.0');
-      expect(imported).toContain('reload_required: false');
+        const listed = String(
+          await marketplace.execute({ action: 'list' }, context),
+        );
+        expect(listed).toContain('community/docs-researcher@1.0.0');
 
 
-      const listed = String(
-        await marketplace.execute({ action: 'list' }, context),
-      );
-      expect(listed).toContain('community/deep-explorer@1.0.0');
-      expect(listed).toContain('community/docs-researcher@1.0.0');
+        const shown = String(
+          await marketplace.execute(
+            {
+              action: 'show',
+              packageId: 'community/docs-researcher',
+            },
+            context,
+          ),
+        );
+        expect(shown).toContain('community/docs-researcher@1.0.0');
 
 
-      const shown = String(
-        await marketplace.execute(
-          {
-            action: 'show',
-            packageId: 'community/docs-researcher',
-          },
-          context,
-        ),
-      );
-      expect(shown).toContain('kind: agent');
-      expect(shown).toContain('role: explorer');
+        const verified = String(
+          await marketplace.execute(
+            {
+              action: 'verify',
+              packageId: 'community/docs-researcher',
+            },
+            context,
+          ),
+        );
+        expect(verified).toContain('OK');
 
 
-      const verified = String(
-        await marketplace.execute(
-          {
-            action: 'verify',
-            packageId: 'community/docs-researcher',
-          },
-          context,
-        ),
-      );
-      expect(verified).toContain('OK');
+        const updated = String(
+          await marketplace.execute(
+            { action: 'import', path: v2Path, update: true },
+            context,
+          ),
+        );
+        expect(updated).toContain('Updated community/docs-researcher@1.1.0');
+        expect(updated).toContain('reload_required: false');
 
 
-      const updated = String(
-        await marketplace.execute(
-          { action: 'import', path: v2Path, update: true },
-          context,
-        ),
-      );
-      expect(updated).toContain('Updated community/docs-researcher@1.1.0');
-      expect(updated).toContain('reload_required: false');
+        const enabled = String(
+          await marketplace.execute(
+            {
+              action: 'enable',
+              packageId: 'community/docs-researcher',
+            },
+            context,
+          ),
+        );
+        expect(enabled).toContain('Enabled community/docs-researcher');
+        expect(enabled).toContain('reload_required: true');
+        expect(enabled).toContain(MARKETPLACE_RELOAD_NOTICE);
+        expect(live.packages).toEqual([]);
 
 
-      const enabled = String(
-        await marketplace.execute(
-          {
-            action: 'enable',
-            packageId: 'community/docs-researcher',
-          },
-          context,
-        ),
-      );
-      expect(enabled).toContain('Enabled community/docs-researcher');
-      expect(enabled).toContain('reload_required: true');
-      expect(enabled).toContain(MARKETPLACE_RELOAD_NOTICE);
-      expect(live.packages).toEqual([]);
+        const statusAfterMutations = String(
+          await marketplace.execute({ action: 'status' }, context),
+        );
+        expect(statusAfterMutations).toContain('reload_required: true');
+        expect(statusAfterMutations).toContain(
+          'community/docs-researcher@1.1.0',
+        );
+        expect(statusAfterMutations).toContain('configured_agents:');
+        expect(statusAfterMutations).toContain('community/docs-researcher');
+        expect(statusAfterMutations).toContain('live_packages:');
+        expect(statusAfterMutations).toContain('  (none)');
+        expect(statusAfterMutations).toContain(MARKETPLACE_RELOAD_NOTICE);
 
 
-      const profiled = String(
-        await marketplace.execute(
-          {
-            action: 'profile',
-            role: 'explorer',
-            packageId: 'community/deep-explorer',
-          },
-          context,
-        ),
-      );
-      expect(profiled).toContain(
-        'Selected community/deep-explorer for explorer',
-      );
-      expect(profiled).toContain(MARKETPLACE_RELOAD_NOTICE);
-
-      const statusAfterMutations = String(
-        await marketplace.execute({ action: 'status' }, context),
-      );
-      expect(statusAfterMutations).toContain('reload_required: true');
-      expect(statusAfterMutations).toContain('community/docs-researcher@1.1.0');
-      expect(statusAfterMutations).toContain('configured_agents:');
-      expect(statusAfterMutations).toContain('community/docs-researcher');
-      expect(statusAfterMutations).toContain('live_packages:');
-      expect(statusAfterMutations).toContain('  (none)');
-      expect(statusAfterMutations).toContain(MARKETPLACE_RELOAD_NOTICE);
-
-      const userConfig = JSON.parse(
-        readFileSync(
-          join(configHome, 'opencode', 'oh-my-opencode-slim.json'),
-          'utf8',
-        ),
-      ) as {
-        presets: {
-          work: {
-            marketplace: {
-              agents: string[];
-              profiles: Record<string, string | null>;
+        const userConfig = JSON.parse(
+          readFileSync(
+            join(configHome, 'opencode', 'oh-my-opencode-slim.json'),
+            'utf8',
+          ),
+        ) as {
+          presets: {
+            work: {
+              marketplace: {
+                agents: string[];
+              };
             };
             };
           };
           };
         };
         };
-      };
-      expect(userConfig.presets.work.marketplace.agents).toEqual([
-        'community/docs-researcher',
-      ]);
-      expect(userConfig.presets.work.marketplace.profiles.explorer).toBe(
-        'community/deep-explorer',
-      );
+        expect(userConfig.presets.work.marketplace.agents).toEqual([
+          'community/docs-researcher',
+        ]);
+        const disabled = String(
+          await marketplace.execute(
+            {
+              action: 'disable',
+              packageId: 'community/docs-researcher',
+            },
+            context,
+          ),
+        );
+        expect(disabled).toContain('Disabled community/docs-researcher');
+        expect(disabled).toContain('reload_required: false');
 
 
-      const cleared = String(
-        await marketplace.execute(
-          { action: 'profile', role: 'explorer', clear: true },
-          context,
-        ),
-      );
-      expect(cleared).toContain('Cleared the explorer profile');
-
-      const disabled = String(
-        await marketplace.execute(
-          {
-            action: 'disable',
-            packageId: 'community/docs-researcher',
-          },
-          context,
-        ),
-      );
-      expect(disabled).toContain('Disabled community/docs-researcher');
-      expect(disabled).toContain('reload_required: false');
-
-      const removed = String(
-        await marketplace.execute(
-          {
-            action: 'remove',
-            packageId: 'community/deep-explorer',
-          },
-          context,
-        ),
-      );
-      expect(removed).toContain('Removed community/deep-explorer');
-      expect(live.packages).toEqual([]);
-    } finally {
-      rmSync(root, { recursive: true, force: true });
-    }
-  });
+        expect(live.packages).toEqual([]);
+      } finally {
+        rmSync(root, { recursive: true, force: true });
+      }
+    },
+    { timeout: 15_000 },
+  );
 
 
   test('reports reload required for an active version update', async () => {
   test('reports reload required for an active version update', async () => {
     const { root, project, service, marketplace, context, live } =
     const { root, project, service, marketplace, context, live } =
@@ -359,7 +262,6 @@ describe('marketplace tool', () => {
           version: installed.manifest.version,
           version: installed.manifest.version,
           digest: installed.digest,
           digest: installed.digest,
           runtimeName: installed.manifest.agentName,
           runtimeName: installed.manifest.agentName,
-          kind: 'agent',
         },
         },
       ];
       ];
       const v2 = writeBundle(project, agentBundle('1.1.0'), 'package-v2.json');
       const v2 = writeBundle(project, agentBundle('1.1.0'), 'package-v2.json');
@@ -392,7 +294,6 @@ describe('marketplace tool', () => {
           version: installed.manifest.version,
           version: installed.manifest.version,
           digest: installed.digest,
           digest: installed.digest,
           runtimeName: installed.manifest.agentName,
           runtimeName: installed.manifest.agentName,
-          kind: 'agent',
         },
         },
       ];
       ];
       const removed = String(
       const removed = String(
@@ -428,7 +329,6 @@ describe('marketplace tool', () => {
           version: installed.manifest.version,
           version: installed.manifest.version,
           digest: installed.digest,
           digest: installed.digest,
           runtimeName: installed.manifest.agentName,
           runtimeName: installed.manifest.agentName,
-          kind: 'agent',
         },
         },
       ];
       ];
       const again = String(
       const again = String(

+ 2 - 69
src/tools/marketplace.ts

@@ -4,7 +4,6 @@ import { z } from 'zod';
 import {
 import {
   disableMarketplacePackage,
   disableMarketplacePackage,
   enableMarketplaceAgent,
   enableMarketplaceAgent,
-  setMarketplaceProfile,
 } from '../marketplace/activation-config';
 } from '../marketplace/activation-config';
 import type { MarketplaceService } from '../marketplace/service';
 import type { MarketplaceService } from '../marketplace/service';
 import {
 import {
@@ -26,7 +25,6 @@ export const MARKETPLACE_TOOL_ACTIONS = [
   'update',
   'update',
   'enable',
   'enable',
   'disable',
   'disable',
-  'profile',
   'remove',
   'remove',
   'status',
   'status',
 ] as const;
 ] as const;
@@ -69,14 +67,6 @@ const MarketplaceToolRequestSchema = z.discriminatedUnion('action', [
       force: z.boolean().optional(),
       force: z.boolean().optional(),
     })
     })
     .strict(),
     .strict(),
-  z
-    .object({
-      action: z.literal('profile'),
-      role: z.string().min(1),
-      packageId: z.string().min(1).optional(),
-      clear: z.literal(true).optional(),
-    })
-    .strict(),
   z.object({ action: z.literal('list') }).strict(),
   z.object({ action: z.literal('list') }).strict(),
   z.object({ action: z.literal('status') }).strict(),
   z.object({ action: z.literal('status') }).strict(),
 ]);
 ]);
@@ -110,15 +100,6 @@ export function parseMarketplaceToolArgs(
     throw new Error(result.error.message);
     throw new Error(result.error.message);
   }
   }
   const request = result.data;
   const request = result.data;
-  if (request.action === 'profile') {
-    if (request.clear === true) {
-      if (request.packageId !== undefined) {
-        throw new Error('profile --clear accepts only a role');
-      }
-    } else if (!request.packageId) {
-      throw new Error('profile requires a role and package ID');
-    }
-  }
   return request;
   return request;
 }
 }
 
 
@@ -145,22 +126,13 @@ function formatPackageLine(pkg: {
   manifest: {
   manifest: {
     id: string;
     id: string;
     version: string;
     version: string;
-    kind: string;
     displayName: string;
     displayName: string;
     description: string;
     description: string;
-    baseRole?: string;
-    targetRole?: string;
   };
   };
   source: unknown;
   source: unknown;
 }): string {
 }): string {
-  const role =
-    pkg.manifest.kind === 'agent'
-      ? pkg.manifest.baseRole
-      : pkg.manifest.targetRole;
   return [
   return [
     `${pkg.manifest.id}@${pkg.manifest.version}`,
     `${pkg.manifest.id}@${pkg.manifest.version}`,
-    `kind: ${pkg.manifest.kind}`,
-    `role: ${role}`,
     `displayName: ${pkg.manifest.displayName}`,
     `displayName: ${pkg.manifest.displayName}`,
     `description: ${pkg.manifest.description}`,
     `description: ${pkg.manifest.description}`,
     `source: ${JSON.stringify(pkg.source)}`,
     `source: ${JSON.stringify(pkg.source)}`,
@@ -177,7 +149,7 @@ Use install and update with a canonical package ID for the default HTTPS registr
 
 
 list, show, verify, and status are read-only and always offline. Remote network work occurs only for explicit install/update. All mutations use the local transaction store and never hot-swap the live agent registry.
 list, show, verify, and status are read-only and always offline. Remote network work occurs only for explicit install/update. All mutations use the local transaction store and never hot-swap the live agent registry.
 
 
-Action-specific fields: packageId for install/update/show/enable/disable/profile/remove; path and optional update=true for import; role plus packageId or role plus clear=true for profile; no extra fields.`,
+Action-specific fields: packageId for install/update/show/enable/disable/remove; path and optional update=true for import; no extra fields.`,
     args: {
     args: {
       action: toolZ
       action: toolZ
         .enum(MARKETPLACE_TOOL_ACTIONS)
         .enum(MARKETPLACE_TOOL_ACTIONS)
@@ -194,19 +166,10 @@ Action-specific fields: packageId for install/update/show/enable/disable/profile
         .describe(
         .describe(
           'Canonical package ID for registry install/update or local actions',
           'Canonical package ID for registry install/update or local actions',
         ),
         ),
-      role: toolZ
-        .string()
-        .min(1)
-        .optional()
-        .describe('Specialist role for profile'),
       force: toolZ
       force: toolZ
         .boolean()
         .boolean()
         .optional()
         .optional()
         .describe('Force remove even if the package is still referenced'),
         .describe('Force remove even if the package is still referenced'),
-      clear: toolZ
-        .boolean()
-        .optional()
-        .describe('Clear the selected profile for a specialist role'),
       update: toolZ
       update: toolZ
         .boolean()
         .boolean()
         .optional()
         .optional()
@@ -277,11 +240,7 @@ Action-specific fields: packageId for install/update/show/enable/disable/profile
           return service
           return service
             .list()
             .list()
             .map((pkg) => {
             .map((pkg) => {
-              const role =
-                pkg.manifest.kind === 'agent'
-                  ? pkg.manifest.baseRole
-                  : pkg.manifest.targetRole;
-              return `${pkg.manifest.id}@${pkg.manifest.version}\t${pkg.manifest.kind}\t${role}\t${pkg.manifest.displayName}`;
+              return `${pkg.manifest.id}@${pkg.manifest.version}\t${pkg.manifest.displayName}`;
             })
             })
             .join('\n');
             .join('\n');
         case 'show':
         case 'show':
@@ -309,32 +268,6 @@ Action-specific fields: packageId for install/update/show/enable/disable/profile
             options,
             options,
             `Disabled ${request.packageId} in the active preset`,
             `Disabled ${request.packageId} in the active preset`,
           );
           );
-        case 'profile':
-          if (request.clear === true) {
-            setMarketplaceProfile(
-              projectDir,
-              request.role,
-              null,
-              service.store,
-            );
-            return mutationResult(
-              options,
-              `Cleared the ${request.role} profile`,
-            );
-          }
-          if (!request.packageId) {
-            throw new Error('profile requires a role and package ID');
-          }
-          setMarketplaceProfile(
-            projectDir,
-            request.role,
-            request.packageId,
-            service.store,
-          );
-          return mutationResult(
-            options,
-            `Selected ${request.packageId} for ${request.role}`,
-          );
         case 'status':
         case 'status':
           return formatMarketplaceStatus(
           return formatMarketplaceStatus(
             collectMarketplaceStatus({
             collectMarketplaceStatus({

+ 0 - 18
src/tools/preset-switch.test.ts

@@ -92,24 +92,6 @@ describe('switchPresetOnDisk', () => {
     expect(result.message).toContain('no agent overrides');
     expect(result.message).toContain('no agent overrides');
   });
   });
 
 
-  test('allows profile-only preset activation', () => {
-    const config: PluginConfig = {
-      presets: {
-        profiles: {
-          agents: {},
-          marketplace: {
-            profiles: { oracle: 'community/oracle-profile' },
-          },
-        },
-      },
-    };
-
-    const result = switchPresetOnDisk(tempDir, 'profiles', config);
-
-    expect(result.ok).toBe(true);
-    expect(result.presetName).toBe('profiles');
-  });
-
   test('switches preset and reports a reload-to-apply message', () => {
   test('switches preset and reports a reload-to-apply message', () => {
     const config: PluginConfig = {
     const config: PluginConfig = {
       presets: {
       presets: {

+ 1 - 3
src/tools/preset-switch.ts

@@ -61,9 +61,7 @@ export function switchPresetOnDisk(
   }
   }
 
 
   const agentUpdates = buildAgentUpdates(preset);
   const agentUpdates = buildAgentUpdates(preset);
-  const hasMarketplaceActivation =
-    preset.marketplace?.agents !== undefined ||
-    preset.marketplace?.profiles !== undefined;
+  const hasMarketplaceActivation = preset.marketplace?.agents !== undefined;
   if (Object.keys(agentUpdates).length === 0 && !hasMarketplaceActivation) {
   if (Object.keys(agentUpdates).length === 0 && !hasMarketplaceActivation) {
     return {
     return {
       ok: false,
       ok: false,