Răsfoiți Sursa

feat(marketplace): adopt agents-only v2 model

Alvin Unreal 4 zile în urmă
părinte
comite
41d538dc68
44 a modificat fișierele cu 2471 adăugiri și 2848 ștergeri
  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
   across 25 languages, and built-in MCPs for docs and GitHub code
 - **[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.
 - **[Fully customizable](docs/configuration.md)** - custom agents, prompt
   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.
 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
-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.
-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.
 
 ```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 update community/example
 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 remove author/name
 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
 selects the highest compatible version, while `ID@version` selects exactly
 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
 orchestrator can perform the same lifecycle
 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`,
 `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:
 
@@ -27,22 +27,19 @@ Presets are structured objects:
   },
   "marketplace": {
     "agents": ["community/example"],
-    "profiles": { "oracle": "community/oracle-profile" }
   }
 }
 ```
 
 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.
 Inline replacement wins over a replacement file, and append files remain
 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
 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
 
@@ -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 |
 | 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 |
-| 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 |

+ 1 - 2
docs/configuration.md

@@ -495,8 +495,7 @@ for the CLI, in-session `marketplace` tool, and status diagnostics.
   "presets": {
     "work": {
       "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
 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.
 
 ## 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 verify [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 remove author/name
 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
 exact `ID@version`; an unversioned ID selects the highest compatible version.
 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
 agents when used from the in-session tool, diagnostics, and whether a
@@ -70,19 +67,18 @@ read (for example EACCES).
 | Field | Meaning |
 |-------|---------|
 | 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 |
 | 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 |
 
 ## 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.
-- 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
   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
 `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
 canonical bundle SHA-256 digest used by the plugin store. The public contract
 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 |
 |---|---|---|
-| **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. |
 | **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. |

+ 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` 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.
 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

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

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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "oh-my-opencode-slim",
-  "version": "3.0.0-beta.2",
+  "version": "3.0.0-beta.3",
   "packageManager": "bun@1.3.14",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "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 {
   type ActivatedMarketplaceAgent,
-  type ActivatedMarketplaceProfile,
-  boundedPackageOverride,
   composePackagePrompt,
   type MarketplaceActivationPlan,
   type MarketplaceDiagnostic,
   reservedRuntimeNames,
   resolveMarketplaceActivation,
 } from '../marketplace/activation';
+import { MARKETPLACE_TOOL_NAMES } from '../marketplace/schemas';
 import type {
   MarketplaceLivePackage,
   MarketplaceLiveSnapshot,
@@ -49,7 +48,6 @@ import {
 import {
   ROLE_DEFINITIONS,
   renderRoleRoutingBlock,
-  type SpecialistRole,
   SUPPORTED_SPECIALIST_ROLES,
 } from './role-definitions';
 import {
@@ -258,22 +256,11 @@ function buildMarketplaceLive(
   marketplace: MarketplaceActivationPlan,
   runtimeNameByPackageId: Readonly<Record<string, string>>,
 ): 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) {
     registered.set(activated.packageId, {
       version: activated.version,
       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[] = [];
@@ -287,7 +274,6 @@ function buildMarketplaceLive(
       version: meta.version,
       digest: meta.digest,
       runtimeName,
-      kind: meta.kind,
     });
   }
   return live;
@@ -363,6 +349,21 @@ function getPrimaryModelFromOverride(
   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 =>
   name === 'councillor' || name.startsWith('councillor-');
 
@@ -443,8 +444,12 @@ function applyOverrides(
   agent: AgentDefinition,
   override: AgentOverrideConfig,
 ): void {
-  if (override.model) {
+  if (override.model !== undefined) {
     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) =>
         typeof m === 'string' ? { id: m } : m,
       );
@@ -478,10 +483,16 @@ function applyOverrides(
         agent.config.variant = primaryModel.variant;
       }
     } 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;
     }
   }
-  if (override.variant) agent.config.variant = override.variant;
+  if (override.variant !== undefined) agent.config.variant = override.variant;
   if (override.temperature !== undefined)
     agent.config.temperature = override.temperature;
   if (override.color) agent.config.color = override.color;
@@ -514,12 +525,25 @@ function applyModelInheritance(
 ): void {
   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;
+    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 hostAgent =
+      runtime.hostAgent(agentName) ?? runtime.hostAgent(resolvedName);
+    const hasHostVariant = hostAgent?.variant !== undefined;
+    const preserveVariant =
+      hasExplicitVariantOverride(override) || hasHostVariant;
     if (override.inheritModelFrom === 'session') {
       delete agentConfig.model;
     } else if (orchestratorModel === undefined) {
@@ -624,6 +653,9 @@ export function applyModelInheritanceToConfig(
     } else {
       agentConfig.model = orchestratorModel;
     }
+    if (!preserveVariant) {
+      delete agentConfig.variant;
+    }
   }
 }
 
@@ -820,61 +852,244 @@ function marketplaceSkillList(
   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,
-  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;
-  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 (
-      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,
   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
@@ -961,15 +1176,12 @@ function marketplaceRoutingGuidance(
   const derived = plan?.agents.find(
     (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;
   return [
     `- Package: ${manifest.displayName}`,
     `- ${manifest.routing.description}`,
-    `- **Delegate when:** ${manifest.routing.delegation.when}`,
+    `- **Delegate when:** ${manifest.routing.when}`,
   ].join('\n');
 }
 
@@ -1122,28 +1334,6 @@ export function createAgents(
 
       const override = getOverrideFromAgents(mergedAgents, name);
       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 ?? '';
 
       agent.config.prompt = resolvePrompt(
@@ -1216,41 +1406,67 @@ export function createAgents(
     (activated) => {
       const name = activated.manifest.agentName;
       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 customPrompts = loadAgentPrompt(name, {
         preset: runtime.preset,
         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.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(
         name,
         override?.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,
         [TASK_REJECTION_INSTRUCTION],
       );
@@ -1300,11 +1516,7 @@ export function createAgents(
     applyModelInheritance(agent, override, configuredOrchestratorModel);
     applyFinalPermissions(
       agent,
-      marketplaceSkillList(
-        agent,
-        override,
-        extraMarketplaceSkills(marketplace, agent.name, agent.baseRole),
-      ),
+      marketplaceSkillList(agent, override, undefined),
       runtime.disabledSkills,
     );
     return agent;
@@ -1352,13 +1564,16 @@ export function createAgents(
       }
     }
     applyModelInheritance(agent, override, configuredOrchestratorModel);
+    const marketplaceEntry = marketplacePackage(marketplace, agent.name);
+    const marketplaceCeilings = marketplaceEntry
+      ? marketplaceCapabilityCeilings(agent, marketplaceEntry, override)
+      : undefined;
+    if (marketplaceCeilings) {
+      applyMarketplaceCapabilities(agent, marketplaceCeilings);
+    }
     applyFinalPermissions(
       agent,
-      marketplaceSkillList(
-        agent,
-        override,
-        extraMarketplaceSkills(marketplace, agent.name, agent.baseRole),
-      ),
+      marketplaceCeilings?.skills,
       runtime.disabledSkills,
     );
     return [agent];
@@ -1552,21 +1767,25 @@ function buildCanonicalAgentConfigs(
   };
 
   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,
       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);
   };
 
@@ -1698,8 +1917,12 @@ export function buildResolvedAgentRegistry(
   const modelArrays = cloneOwned(runtime.modelArrays);
   for (const activated of marketplace.agents) {
     const name = activated.manifest.agentName;
-    if (modelArrays[name]) continue;
     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 =
       typeof agent?.config.model === 'string' ? agent.config.model : undefined;
     if (model) {
@@ -1723,28 +1946,80 @@ export function buildResolvedAgentRegistry(
     const hostConfig = hostAgentFor(runtime, runtimeName, name);
     const hostModel = resolvePrimaryModelValue(hostConfig?.model);
     const configuredModels = modelArrays[name];
-    if (
+    const hostVariant = stringVariant(hostConfig?.variant);
+    const hostReplacesModel = Boolean(
       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] = [
         {
           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 &
       Record<string, unknown>;
     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;
   }
 
@@ -1752,6 +2027,31 @@ export function buildResolvedAgentRegistry(
   // config hook must not repeat this resolution against its host projection.
   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 skillPermissions: Record<
     string,
@@ -1799,6 +2099,9 @@ export function buildResolvedAgentRegistry(
       availableMcpNames,
     ) as SDKAgentConfig['permission'];
     const permission = sdkConfig.permission;
+    const marketplaceAgent = marketplace.agents.find(
+      (entry) => entry.manifest.agentName === agent.name,
+    );
     skillPermissions[agent.name] =
       typeof permission === 'object' &&
       permission !== null &&
@@ -1807,34 +2110,25 @@ export function buildResolvedAgentRegistry(
         ? cloneOwned(
             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
       ? `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;
     canonicalIdByRuntimeName[agent.name] = agent.name;
     if (marketplaceAgent) {
       packageIdByRuntimeName[agent.name] = marketplaceAgent.packageId;
       runtimeNameByPackageId[marketplaceAgent.packageId] = agent.name;
-    } else if (marketplaceProfile && isSubagent(agent.name)) {
-      packageIdByRuntimeName[agent.name] = marketplaceProfile.packageId;
-      runtimeNameByPackageId[marketplaceProfile.packageId] = agent.name;
     }
     if (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 description: string;
   readonly derivable: true;
-  readonly profileTargetable: true;
   readonly defaultModel: string | undefined;
   readonly defaultSkills: readonly string[];
   readonly defaultMcps: readonly string[];
@@ -346,7 +345,6 @@ function defineRole(
     basePrompt,
     description,
     derivable: true,
-    profileTargetable: true,
     defaultModel,
     defaultSkills: Object.freeze([...defaultSkills]),
     defaultMcps: Object.freeze([...defaultMcps]),

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

@@ -448,7 +448,6 @@ function configMutationPaths(filePath: string): MarketplacePaths {
     packagesDir: join(root, 'packages'),
     lockfilePath: join(root, 'lock.json'),
     lockDir: join(root, 'lock'),
-    breakerDir: join(root, 'lock.breaker'),
     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 enable <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]
 
 Options:

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

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

+ 5 - 58
src/cli/marketplace.ts

@@ -5,7 +5,6 @@ import {
 import {
   disableMarketplacePackage,
   enableMarketplaceAgent,
-  setMarketplaceProfile,
 } from '../marketplace/activation-config';
 import {
   collectMarketplaceStatus,
@@ -23,16 +22,13 @@ export type MarketplaceCommandName =
   | 'remove'
   | 'enable'
   | 'disable'
-  | 'profile'
   | 'status';
 
 export interface MarketplaceArgs {
   command: MarketplaceCommandName;
   value?: string;
-  role?: string;
   force: boolean;
   json: boolean;
-  clear: boolean;
   update?: boolean;
 }
 
@@ -47,7 +43,6 @@ function commandSet(): readonly string[] {
     'remove',
     'enable',
     'disable',
-    'profile',
     'status',
   ];
 }
@@ -56,13 +51,12 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
   const rawCommand = args[0];
   if (!rawCommand || !commandSet().includes(rawCommand)) {
     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 force = args.includes('--force');
   const json = args.includes('--json');
-  const clear = args.includes('--clear');
   const update = args.includes('--update');
   const options = args.filter((arg) => arg.startsWith('--'));
   const allowedOptions = new Set(
@@ -70,11 +64,9 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
       ? ['--force']
       : command === 'verify' || command === 'show' || command === 'status'
         ? ['--json']
-        : command === 'profile'
-          ? ['--clear']
-          : command === 'import'
-            ? ['--update']
-            : [],
+        : command === 'import'
+          ? ['--update']
+          : [],
   );
   for (const option of options) {
     if (!allowedOptions.has(option)) {
@@ -85,34 +77,6 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
   }
 
   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 = [
     'install',
     'import',
@@ -147,7 +111,6 @@ export function parseMarketplaceArgs(args: string[]): MarketplaceArgs {
     value: positional[0],
     force,
     json,
-    clear,
     ...(command === 'import' && update ? { update: true } : {}),
   };
 }
@@ -196,7 +159,7 @@ export async function marketplaceCommand(
       case 'list':
         for (const pkg of service.list()) {
           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;
@@ -245,22 +208,6 @@ export async function marketplaceCommand(
           ),
         );
         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': {
         const report = collectMarketplaceStatus({
           service,

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

@@ -33,12 +33,11 @@ describe('typed agent and preset merge precedence', () => {
     ).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 = {
       agents: {},
       marketplace: {
         agents: ['community/one'],
-        profiles: { oracle: 'community/oracle' },
       },
     } satisfies Preset;
     expect(mergePreset(base, { agents: {} })).toEqual(base);
@@ -48,19 +47,16 @@ describe('typed agent and preset merge precedence', () => {
       agents: {},
       marketplace: {
         agents: [],
-        profiles: { oracle: 'community/oracle' },
       },
     });
     expect(
       mergePreset(base, {
         agents: {},
-        marketplace: { profiles: { oracle: null } },
       }),
     ).toEqual({
       agents: {},
       marketplace: {
         agents: ['community/one'],
-        profiles: { oracle: null },
       },
     });
   });

+ 2 - 9
src/config/loader.ts

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

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

@@ -7,12 +7,11 @@ import {
 } from './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({
       agents: { explorer: { model: 'provider/explorer' } },
       marketplace: {
         agents: ['community/example'],
-        profiles: { librarian: null },
       },
     });
     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({
       marketplace: {
         agents: ['  community/one  '],
-        profiles: { oracle: '  community/oracle  ' },
       },
     });
     expect(result.success).toBe(true);
     if (result.success) {
       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({
       marketplace: {
         agents: ['community/one', ' community/one '],
-        profiles: { orchestrator: 'community/one' },
+        removed: 'community/one',
       },
     });
     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. */
 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 MarketplacePackageIdsSchema = z
   .array(MarketplacePackageIdSchema)
@@ -171,44 +166,6 @@ const MarketplacePackageIdsSchema = z
 export const MarketplaceActivationSchema = z
   .object({
     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();
 

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

@@ -12,6 +12,7 @@ import {
   MARKETPLACE_DIGEST_DOMAIN,
   MARKETPLACE_MANIFEST_SCHEMA_VERSION,
   MarketplaceAgentManifestSchema,
+  MarketplaceAgentManifestSummarySchema,
   type MarketplaceDigest,
   MarketplaceDigestSchema,
   type MarketplacePackageBundle,
@@ -20,7 +21,6 @@ import {
   MarketplacePackageIdSchema,
   type MarketplacePackageManifest,
   MarketplacePackageManifestSchema,
-  MarketplaceProfileManifestSchema,
   type MarketplaceVersion,
   MarketplaceVersionSchema,
 } from '../marketplace/schemas';
@@ -44,26 +44,17 @@ export {
   MarketplacePackageBundleSchema,
   MarketplacePackageIdSchema,
   MarketplacePackageManifestSchema,
-  MarketplaceProfileManifestSchema,
   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 =
-  '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
   .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({
     schemaVersion: z.literal(MARKETPLACE_REGISTRY_SCHEMA_VERSION),
     entries: z.array(MarketplaceRegistryEntrySchema).max(100_000),
@@ -159,7 +148,6 @@ const MarketplaceRegistryIndexV2Schema = z
   .strict()
   .superRefine((index, ctx) => {
     validateRegistryEntries(index.entries, ctx);
-    const entryIds = new Set(index.entries.map((entry) => entry.id));
     const seen = new Set<string>();
     for (let position = 0; position < index.retirements.length; position += 1) {
       const retirement = index.retirements[position];
@@ -171,13 +159,6 @@ const MarketplaceRegistryIndexV2Schema = z
         });
       }
       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];
       if (
         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<
   typeof MarketplaceManifestSummarySchema
@@ -225,7 +212,7 @@ export function projectMarketplaceManifestSummary(
   manifest: MarketplacePackageManifest,
 ): MarketplaceManifestSummary {
   const parsed = MarketplacePackageBundleSchema.shape.manifest.parse(manifest);
-  const { instructions: _instructions, ...summary } = parsed;
+  const { prompt: _prompt, ...summary } = parsed;
   return summary as MarketplaceManifestSummary;
 }
 
@@ -251,7 +238,9 @@ export function createMarketplaceRegistryEntry(
 
 export function createMarketplaceRegistryIndex(
   entries: readonly MarketplaceRegistryEntry[],
-  retirements: readonly MarketplaceRegistryRetirement[] = [],
+  retirements: readonly MarketplaceRegistryRetirement[] = CANONICAL_MARKETPLACE_RETIREMENT_IDS.map(
+    (id) => ({ id }),
+  ),
 ): MarketplaceRegistryIndex {
   const sorted = [...entries].sort(
     (left, right) =>
@@ -272,9 +261,7 @@ export function createMarketplaceRegistryIndex(
 export function retiredMarketplaceRegistryIds(
   index: MarketplaceRegistryIndex,
 ): ReadonlySet<string> {
-  return new Set(
-    index.schemaVersion === 2 ? index.retirements.map(({ id }) => id) : [],
-  );
+  return new Set(index.retirements.map(({ id }) => id));
 }
 
 export function isMarketplaceRegistryIdRetired(
@@ -363,7 +350,7 @@ export function parseMarketplaceRegistrySelector(
 export function resolveMarketplaceRegistryEntry(
   index: MarketplaceRegistryIndex,
   selector: MarketplaceRegistrySelector,
-  compatibility: { pluginVersion: string; roleContractVersion: string },
+  compatibility: { pluginVersion: string },
   minimumVersion?: string,
 ): MarketplaceRegistryEntry {
   if (
@@ -382,10 +369,6 @@ export function resolveMarketplaceRegistryEntry(
       satisfies(
         compatibility.pluginVersion,
         entry.summary.compatibility.plugin,
-      ) &&
-      satisfies(
-        compatibility.roleContractVersion,
-        entry.summary.compatibility.roleContract,
       ),
   );
   const selected = [...candidates].sort(

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

@@ -1,8 +1,4 @@
 import { mutateJsonFile } from '../cli/config-io';
-import {
-  type SpecialistRole,
-  SUPPORTED_SPECIALIST_ROLES,
-} from '../config/agent-roles';
 import {
   findPluginConfigPaths,
   loadPluginConfig,
@@ -57,7 +53,6 @@ function cloneActivation(
 ): MarketplaceActivation {
   return {
     ...(activation?.agents ? { agents: [...activation.agents] } : {}),
-    ...(activation?.profiles ? { profiles: { ...activation.profiles } } : {}),
   };
 }
 
@@ -78,27 +73,6 @@ function activationOverlay(
   if (parent === undefined || !samePackageIds(parent.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;
 }
 
@@ -144,26 +118,13 @@ export function enableMarketplaceAgent(
 ): void {
   const id = normalizeMarketplacePackageId(packageId);
   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 presetName = activePresetName(config);
   persistActivation(directory, presetName, (activation) => {
     const agents = [...(activation.agents ?? [])];
     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);
   persistActivation(directory, presetName, (activation) => ({
     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 {
   disableMarketplacePackage,
   enableMarketplaceAgent,
-  setMarketplaceProfile,
 } from './activation-config';
 import type { MarketplacePackageBundle } from './schemas';
 import { MarketplaceStore } from './store';
@@ -39,76 +38,29 @@ function agentBundle(
 ): MarketplacePackageBundle {
   return {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/docs-researcher',
       version: '1.0.0',
-      kind: 'agent',
       displayName: 'Docs researcher',
       description: 'A derived explorer.',
-      instructions: 'Prefer documentation paths first.',
+      agentName: 'docsresearcher',
+      prompt: 'Prefer documentation paths first.',
       author: { name: 'Community' },
       tags: ['docs'],
       license: 'MIT',
       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: {
         description: 'Research docs and examples.',
         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,
     } as MarketplacePackageBundle['manifest'],
   };
@@ -188,7 +140,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          overrides: { model: 'package/model', description: 'Package desc' },
+          model: { source: 'explicit', candidates: ['package/model'] },
+          description: 'Package desc',
         }),
       );
       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', () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     try {
@@ -362,10 +220,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: ['not-a-real-skill'], optional: ['simplify'] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['not-a-real-skill'],
+          mcps: [],
         }),
       );
       const registry = registryFor(
@@ -395,10 +251,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: ['not-a-real-skill'] },
-            mcps: { required: [], optional: ['unknown-mcp'] },
-          },
+          skills: [],
+          mcps: [],
         }),
       );
       const registry = registryFor(
@@ -569,10 +423,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['context7'], optional: [] },
-          },
+          skills: [],
+          mcps: ['context7'],
         }),
       );
       RuntimeConfig.reset('marketplace-mcp-projection');
@@ -616,10 +468,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['injected-plugin-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['injected-plugin-mcp'],
         }),
       );
       RuntimeConfig.reset('marketplace-injected-mcp');
@@ -665,10 +515,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: [],
         }),
       );
       RuntimeConfig.reset('marketplace-disk-skill');
@@ -736,10 +584,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['injected-host-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['injected-host-mcp'],
         }),
       );
       RuntimeConfig.reset('marketplace-host-mcp');
@@ -792,10 +638,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['project-mcp'],
         }),
       );
       RuntimeConfig.reset(project);
@@ -838,10 +682,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['project-mcp'],
         }),
       );
       RuntimeConfig.reset(project);
@@ -901,10 +743,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: ['project-mcp'], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: ['project-mcp'],
         }),
       );
       RuntimeConfig.reset(project);
@@ -962,10 +802,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['empty-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['empty-mcp'],
         }),
       );
       RuntimeConfig.reset(project);
@@ -987,10 +825,8 @@ describe('marketplace runtime activation', () => {
         agentBundle({
           id: 'community/config-json',
           agentName: 'configjson',
-          requirements: {
-            skills: { required: [], optional: [] },
-            mcps: { required: ['config-json-mcp'], optional: [] },
-          },
+          skills: [],
+          mcps: ['config-json-mcp'],
         }),
       );
       RuntimeConfig.reset(`${project}-ok`);
@@ -1023,10 +859,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: ['hidden-skill'], optional: [] },
-            mcps: { required: [], optional: [] },
-          },
+          skills: ['hidden-skill'],
+          mcps: [],
         }),
       );
       RuntimeConfig.reset(root);
@@ -1058,10 +892,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         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');
@@ -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-'));
     try {
       const store = new MarketplaceStore({ rootDir: root });
-      store.install(
-        agentBundle({
-          overrides: { displayName: 'janitor' },
-        }),
-      );
+      store.install(agentBundle());
       const colliding = registryFor(
         {
           preset: 'work',
-          agents: { janitor: { model: 'custom/model' } },
+          agents: {
+            docsresearcher: { displayName: 'janitor' },
+            janitor: { model: 'custom/model' },
+          },
           presets: {
             work: {
               agents: { janitor: { model: 'custom/model' } },
@@ -1114,10 +945,7 @@ describe('marketplace runtime activation', () => {
       expect(colliding.diagnostics[0]?.code).toBe('collision');
 
       store.install(
-        agentBundle({
-          id: 'community/janitor',
-          agentName: 'janitor',
-        }),
+        agentBundle({ id: 'community/janitor', agentName: 'janitor' }),
       );
       const ownKey = registryFor(
         {
@@ -1149,7 +977,7 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          overrides: { displayName: 'docsresearcher' },
+          agentName: 'docsresearcher',
         }),
       );
       const registry = registryFor(
@@ -1157,7 +985,7 @@ describe('marketplace runtime activation', () => {
           preset: 'work',
           presets: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'docsresearcher' } },
               marketplace: { agents: ['community/docs-researcher'] },
             },
           },
@@ -1203,10 +1031,8 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          requirements: {
-            skills: { required: ['on-disk-skill'], optional: [] },
-            mcps: { required: ['config-mcp'], optional: [] },
-          },
+          skills: ['on-disk-skill'],
+          mcps: ['config-mcp'],
         }),
       );
       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', () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-activation-'));
     try {
       const store = new MarketplaceStore({ rootDir: root });
-      store.install(
-        agentBundle({
-          overrides: { displayName: 'Docs Researcher' },
-        }),
-      );
+      store.install(agentBundle());
       const registry = registryFor(
         {
           preset: 'work',
           presets: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'Docs Researcher' } },
               marketplace: { agents: ['community/docs-researcher'] },
             },
           },
@@ -1347,7 +1132,7 @@ describe('marketplace runtime activation', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(
         agentBundle({
-          overrides: { displayName: 'docsalias', model: 'package/model' },
+          model: { source: 'explicit', candidates: ['package/model'] },
         }),
       );
       const registry = registryFor(
@@ -1355,7 +1140,7 @@ describe('marketplace runtime activation', () => {
           preset: 'work',
           presets: {
             work: {
-              agents: {},
+              agents: { docsresearcher: { displayName: 'docsalias' } },
               marketplace: { agents: ['community/docs-researcher'] },
             },
           },
@@ -1376,12 +1161,12 @@ describe('marketplace runtime activation', () => {
         expect.objectContaining({
           packageId: 'community/docs-researcher',
           runtimeName: 'docsalias',
-          kind: 'agent',
         }),
       ]);
       expect(
-        (registry.sdkConfigs.docsalias.permission as Record<string, unknown>)
-          .marketplace,
+        (registry.sdkConfigs.docsalias.permission as Record<string, unknown>)[
+          '*'
+        ],
       ).toBe('deny');
       expect(
         (
@@ -1389,7 +1174,7 @@ describe('marketplace runtime activation', () => {
             string,
             unknown
           >
-        ).marketplace,
+        )['*'],
       ).toBe('deny');
       expect(registry.sdkConfigs.docsalias.model).toBe('package/model');
       expect(registry.sdkConfigs.docsresearcher.model).toBe('package/model');
@@ -1423,7 +1208,6 @@ describe('marketplace runtime activation', () => {
         expect.objectContaining({
           packageId: 'community/docs-researcher',
           runtimeName: 'fieldscout',
-          kind: 'agent',
         }),
       ]);
     } finally {
@@ -1453,7 +1237,13 @@ describe('marketplace runtime activation', () => {
     const sessionDir = `${project}\0session`;
     try {
       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);
       const session = RuntimeConfig.init(sessionDir, {
         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 () => {
     const root = mkdtempSync(join(tmpdir(), 'marketplace-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 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 { validateMarketplaceCompatibility } from './compatibility';
 import {
@@ -19,11 +15,7 @@ import {
   discoverPreflightSkills,
 } from './preflight';
 import { isMarketplacePackageRetired } from './retirements';
-import type {
-  MarketplaceAgentManifest,
-  MarketplacePackageManifest,
-  MarketplaceProfileManifest,
-} from './schemas';
+import type { MarketplaceAgentManifest } from './schemas';
 import { MarketplaceStore, type StoredMarketplacePackage } from './store';
 
 export {
@@ -39,11 +31,8 @@ export type MarketplaceDiagnosticCode =
   | 'incompatible'
   | 'collision'
   | 'missing-required-dependency'
-  | 'kind-mismatch'
-  | 'target-mismatch'
-  | 'target-disabled'
-  | 'prompt-masked'
   | 'invalid-alias'
+  | 'invalid-capability'
   | 'retired';
 
 export interface MarketplaceDiagnostic {
@@ -61,18 +50,8 @@ export interface ActivatedMarketplaceAgent {
   requiredMcps: readonly string[];
 }
 
-export interface ActivatedMarketplaceProfile {
-  packageId: string;
-  version: string;
-  digest: string;
-  manifest: MarketplaceProfileManifest;
-  requiredSkills: readonly string[];
-  requiredMcps: readonly string[];
-}
-
 export interface MarketplaceActivationPlan {
   agents: ActivatedMarketplaceAgent[];
-  profiles: Map<SpecialistRole, ActivatedMarketplaceProfile>;
   diagnostics: MarketplaceDiagnostic[];
 }
 
@@ -86,7 +65,7 @@ export interface ResolveMarketplaceActivationOptions {
 }
 
 export function emptyMarketplaceActivationPlan(): MarketplaceActivationPlan {
-  return { agents: [], profiles: new Map(), diagnostics: [] };
+  return { agents: [], diagnostics: [] };
 }
 
 export function marketplaceActivationFromRuntime(
@@ -97,33 +76,15 @@ export function marketplaceActivationFromRuntime(
   return runtime.plugin?.presets?.[presetName]?.marketplace;
 }
 
+/** Compose only the package-owned extension layers. */
 export function composePackagePrompt(
-  rolePrompt: string,
-  instructions: string,
+  builtinPrompt: string,
+  packagePrompt: string,
   mode: 'append' | 'replace',
 ): 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(
@@ -145,12 +106,10 @@ export function reservedRuntimeNames(
     }
   }
   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())) {
-    if (name === ownOverrideKey) continue;
-    if (override.displayName) {
+    if (name !== ownOverrideKey && override.displayName) {
       reserved.add(normalizeAgentName(override.displayName));
     }
   }
@@ -165,13 +124,6 @@ function diagnostic(
   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 {
   if (
     error instanceof MarketplaceIntegrityError &&
@@ -188,59 +140,80 @@ function marketplaceLoadFailureCode(error: unknown): MarketplaceDiagnosticCode {
   return 'operational';
 }
 
-function claimDisplayAlias(
+function missing(
+  values: readonly string[],
+  available: ReadonlySet<string>,
+): string[] {
+  return values.filter((value) => !available.has(value));
+}
+
+function claimName(
   packageId: string,
-  runtimeName: string,
-  rawDisplayName: string | undefined,
+  name: string,
   reserved: ReadonlySet<string>,
-  claimedNames: Set<string>,
+  claimed: Set<string>,
 ): MarketplaceDiagnostic | undefined {
-  if (!rawDisplayName) return undefined;
-  const displayName = normalizeAgentName(rawDisplayName);
-  if (!displayName || displayName === runtimeName) return undefined;
-  if (!isSafeAgentAlias(displayName)) {
+  if (!isSafeAgentAlias(name)) {
     return diagnostic(
       packageId,
       '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(
       packageId,
       '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;
 }
 
+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(
   options: ResolveMarketplaceActivationOptions,
 ): MarketplaceActivationPlan {
   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 store = options.store ?? new MarketplaceStore();
-  const disabledSkills = new Set(options.runtime.disabledSkills);
-  const disabledMcps = new Set(options.runtime.disabledMcps);
   const skills = new Set(
     (
       options.availableSkillNames ??
@@ -249,42 +222,21 @@ export function resolveMarketplaceActivation(
         options.projectDirectory,
         options.extraSkillDirectories,
       )
-    ).filter((name) => !disabledSkills.has(name)),
+    ).filter((name) => !options.runtime.disabledSkills.includes(name)),
   );
   const mcps = new Set(
     (
       options.availableMcpNames ??
       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 {
-    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) {
-    selectedLoadFailed = true;
     diagnostics.push(
       diagnostic(
         '(store)',
@@ -292,199 +244,89 @@ export function resolveMarketplaceActivation(
         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 {
-      normalized = normalizeMarketplacePackageId(packageId);
+      id = normalizeMarketplacePackageId(packageId);
     } 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(
-        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(
-        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;
     }
-    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(
-        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;
     }
-    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;
     }
-    if (pkg.manifest.targetRole !== role) {
+    try {
+      validateMarketplaceCompatibility(pkg.manifest);
+    } catch (error) {
       diagnostics.push(
         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;
     }
-    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(
         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;
     }
-    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;
     }
-    profiles.set(role, {
-      packageId: pkg.manifest.id,
+    claimed.add(name);
+    agents.push({
+      packageId: id,
       version: pkg.manifest.version,
       digest: pkg.digest,
       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 = {
   manifest: {
-    schemaVersion: 1,
+    schemaVersion: 2,
     id: 'community/compatible',
     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' },
     tags: ['test'],
     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: {
       description: 'Use for compatibility tests.',
       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);
   });
 
-  test('rejects incompatible packages before persistence', () => {
+  test('rejects incompatible agents before persistence', () => {
     const service = new MarketplaceService({
       rootDir: '/tmp/marketplace-compatibility-test',
       pluginVersion: '1.0.0',

+ 1 - 12
src/marketplace/compatibility.ts

@@ -1,14 +1,10 @@
 import { satisfies, valid } from 'semver';
 import { readPluginPackageVersion } from '../utils/package-metadata';
 import { MarketplaceCompatibilityError } from './errors';
-import {
-  MARKETPLACE_ROLE_CONTRACT_VERSION,
-  type MarketplacePackageManifest,
-} from './schemas';
+import type { MarketplacePackageManifest } from './schemas';
 
 export interface MarketplaceCompatibilityOptions {
   pluginVersion?: string;
-  roleContractVersion?: string;
 }
 
 export function satisfiesPluginCompatibility(
@@ -28,8 +24,6 @@ export function validateMarketplaceCompatibility(
       'Cannot determine the installed plugin version for marketplace compatibility',
     );
   }
-  const roleContractVersion =
-    options.roleContractVersion ?? MARKETPLACE_ROLE_CONTRACT_VERSION;
   if (
     !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}`,
     );
   }
-  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 = {
   manifest: {
-    schemaVersion: 1,
+    schemaVersion: 2,
     id: 'community/referenced',
     version: '1.0.0',
-    kind: 'agent',
     displayName: 'Referenced agent',
     description: 'An agent used by a preset.',
-    instructions: 'Follow the role instructions.',
+    agentName: 'referenced',
+    prompt: 'Follow the role instructions.',
     author: { name: 'Community' },
     tags: ['test'],
     license: 'MIT',
     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: {
       description: 'Route referenced work here.',
       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;
   configPath: string;
   presetName: string;
-  target: 'agent' | 'profile';
+  target: 'agent';
 }
 
 interface UnknownRecord {
@@ -20,7 +20,7 @@ function addReference(
   value: unknown,
   configPath: string,
   presetName: string,
-  target: 'agent' | 'profile',
+  target: 'agent',
 ): void {
   if (typeof value !== 'string' || value.trim().length === 0) return;
   references.push({
@@ -50,13 +50,6 @@ function readReferencesFromMergedConfig(
       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;
 }
 

+ 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 {
+  closeSync,
   existsSync,
+  lstatSync,
   mkdirSync,
   mkdtempSync,
+  openSync,
   readdirSync,
   readFileSync,
   rmSync,
+  symlinkSync,
   utimesSync,
   writeFileSync,
 } from 'node:fs';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
-import { MarketplaceBusyError, MarketplaceLockOwnershipError } from './errors';
 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,
-  heartbeatMs: 20,
   timeoutMs: 400,
   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 {
   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 {
-    return readdirSync(directory).filter((name) =>
-      name.startsWith('.creating.'),
-    );
+    return readdirSync(lockDir);
   } catch {
     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,
-  payload: Record<string, unknown>,
-  env: Record<string, string | undefined> = {},
+  extra: Record<string, string> = {},
 ): Bun.Subprocess {
   return Bun.spawn(
     [
       'bun',
       '-e',
-      `import { existsSync, writeFileSync } from 'node:fs';
-import { acquireMarketplaceLease } from './src/marketplace/lease.ts';
+      `import { acquireMarketplaceLease } from './src/marketplace/lease.ts';
 ${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(),
     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 paths = getMarketplacePaths(root);
     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 {
       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 paths = getMarketplacePaths(root);
     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 {
       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 paths = getMarketplacePaths(root);
     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 {
       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 paths = getMarketplacePaths(root);
     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 {
       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 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 {
-      const owner = acquireMarketplaceLeaseForTests(
+      const owner = acquireMarketplaceLease(paths, LOCK);
+      challenger = spawnWorker(
         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();
+      await challenger.exited;
+      expect(readFileSync(resultPath, 'utf8')).toBe('MarketplaceBusyError');
     } finally {
+      challenger?.kill();
+      if (challenger) await challenger.exited.catch(() => -1);
       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 paths = getMarketplacePaths(root);
     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 {
       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 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();`,
-      { paths, readyPath, holdingPath, goPath, lock: SHORT_LOCK },
+        { barrierDir, goPath },
+      ),
     );
     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');
-      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 {
-      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 });
     }
   });
 
-  test('kills a worker during private generation initialization', async () => {
+  test('multiple reclaimers exclude one another', async () => {
     const root = tempRoot();
     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 {
-      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 {
-      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 });
     }
   });
 
-  test('expired owner commit fails before replacement', async () => {
+  test('replacement fencing unlinks only the exact unique lease path', () => {
     const root = tempRoot();
     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 {
-      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 {
-      worker.kill();
-      await worker.exited.catch(() => undefined);
       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 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 {
-      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 {
-      worker.kill();
-      await worker.exited.catch(() => undefined);
       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 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 {
-      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 {
-      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(() =>
-        acquireMarketplaceLease(paths, {
-          staleMs: 2_000,
-          heartbeatMs: 100,
-          timeoutMs: 120,
-          retryMs: 10,
-        }),
+        acquireMarketplaceLease(paths, { ...LOCK, timeoutMs: 60 }),
       ).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 {
-      owner.kill();
-      staleClaim?.kill();
-      await Promise.all([
-        owner.exited.catch(() => undefined),
-        staleClaim?.exited.catch(() => undefined),
-      ]);
       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 {
   staleMs: number;
   timeoutMs: number;
-  heartbeatMs: number;
   retryMs: number;
 }
 
 export interface MarketplaceLease {
-  generation: string;
-  token: string;
   assertCurrent(): void;
   commit<T>(operation: () => T): T;
   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 = {
   staleMs: 60_000,
   timeoutMs: 300_000,
-  heartbeatMs: 20_000,
   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 {
@@ -64,6 +60,22 @@ function errnoCode(error: unknown): string | 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 {
   try {
     const fd = fs.openSync(directory, 'r');
@@ -95,33 +107,7 @@ export function writeAtomic(filePath: string, content: string): void {
     try {
       if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
     } 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 (
     !Number.isFinite(options.staleMs) ||
     !Number.isFinite(options.timeoutMs) ||
-    !Number.isFinite(options.heartbeatMs) ||
     !Number.isFinite(options.retryMs) ||
     options.staleMs <= 0 ||
     options.timeoutMs < 0 ||
-    options.heartbeatMs <= 0 ||
-    options.retryMs <= 0 ||
-    options.heartbeatMs >= options.staleMs
+    options.retryMs <= 0
   ) {
     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 {
     staleMs: options.staleMs,
     timeoutMs: options.timeoutMs,
-    heartbeatMs: options.heartbeatMs,
     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 {
-    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 {
-    entries = fs.readdirSync(directory, { withFileTypes: true });
+    fs.unlinkSync(filePath);
+    syncDirectory(path.dirname(filePath));
   } catch (error) {
     if (errnoCode(error) === 'ENOENT') return;
     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 {
-    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 {
-      if (fs.existsSync(privateDirectory)) {
-        fs.rmSync(privateDirectory, { recursive: true, force: true });
-      }
+      if (fs.existsSync(candidate.path)) unlinkExact(candidate.path);
     } 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 {
-    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 {
-    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 {
-      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 {
-    ...identity,
     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,
   };
@@ -596,52 +383,15 @@ function createMarketplaceLease(
 function acquireMarketplaceLeaseInternal(
   paths: MarketplacePaths,
   partialOptions: Partial<MarketplaceLockOptions> = {},
-  now?: () => number,
+  hooks: MarketplaceLeaseTestHooks = {},
 ): MarketplaceLease {
   const options = normalizeLockOptions(partialOptions);
   fs.mkdirSync(paths.rootDir, { recursive: true });
+  fs.mkdirSync(paths.lockDir, { recursive: true });
   const deadline = Date.now() + options.timeoutMs;
   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;
     sleepSync(options.retryMs);
   }
@@ -651,17 +401,9 @@ function acquireMarketplaceLeaseInternal(
 export function acquireMarketplaceLease(
   paths: MarketplacePaths,
   partialOptions: Partial<MarketplaceLockOptions> = {},
+  hooks: MarketplaceLeaseTestHooks = {},
 ): 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>(

+ 0 - 2
src/marketplace/paths.ts

@@ -6,7 +6,6 @@ export interface MarketplacePaths {
   packagesDir: string;
   lockfilePath: string;
   lockDir: string;
-  breakerDir: string;
   stagingDir: string;
 }
 
@@ -25,7 +24,6 @@ export function getMarketplacePaths(rootDir?: string): MarketplacePaths {
     packagesDir: join(root, 'packages'),
     lockfilePath: join(root, 'lock.json'),
     lockDir: join(root, 'marketplace.lock'),
-    breakerDir: join(root, 'marketplace.lock.breaker'),
     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 {
   return {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/dispatch-agent',
       version,
-      kind: 'agent',
       displayName: 'Dispatch agent',
       description: 'A dispatch test package.',
-      instructions: 'Use the explorer role.',
+      agentName: 'dispatchagent',
+      prompt: 'Use the explorer role.',
       author: { name: 'Community' },
       tags: ['dispatch'],
       license: 'MIT',
-      compatibility: { plugin: '>=3.0.0', roleContract: '^1.0.0' },
+      compatibility: { plugin: '>=3.0.0' },
       routing: {
         description: 'Dispatch test routing.',
         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,
     entry: createMarketplaceRegistryEntry(packageBundle),
     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';
 import { MarketplaceService } from './service';
 
+const canonicalRetirements = [
+  { id: 'alvin/deepwork-implementer' },
+  { id: 'alvin/deepwork-recon' },
+  { id: 'alvin/deepwork-reviewer' },
+];
+
 function bundle(
   version = '1.0.0',
   id = 'community/registry-agent',
@@ -37,33 +43,26 @@ function bundle(
 ): MarketplacePackageBundle {
   return {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id,
       version,
-      kind: 'agent',
       displayName: 'Registry agent',
       description: 'A registry test package.',
-      instructions: 'Use the explorer role.',
+      agentName: 'registryagent',
+      prompt: 'Use the explorer role.',
       author: { name: 'Community' },
       tags: ['registry'],
       license: 'MIT',
-      compatibility: { plugin, roleContract: '^1.0.0' },
+      compatibility: { plugin },
       routing: {
         description: 'Explore registry fixtures.',
         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> {
   const manifest = packageBundle.manifest;
   return {
-    schemaVersion: 1,
+    schemaVersion: 3,
     entries: [
       {
         id: manifest.id,
@@ -79,12 +78,13 @@ function indexFor(packageBundle = bundle()): Record<string, unknown> {
         artifactPath: registryArtifactPath(manifest.id, manifest.version),
         digest: {
           algorithm: 'sha256',
-          domain: 'marketplace-bundle-v1',
+          domain: 'marketplace-agent-bundle-v2',
           value: digestMarketplaceBundle(packageBundle),
         },
         summary: projectMarketplaceManifestSummary(manifest),
       },
     ],
+    retirements: canonicalRetirements,
   };
 }
 
@@ -100,11 +100,12 @@ describe('marketplace registry contract', () => {
     const first = indexFor(bundle('1.0.0'));
     const second = indexFor(bundle('2.0.0'));
     const validIndex = {
-      schemaVersion: 1,
+      schemaVersion: 3,
       entries: [
         ...(first.entries as unknown[]),
         ...(second.entries as unknown[]),
       ],
+      retirements: canonicalRetirements,
     };
     expect(MarketplaceRegistryIndexSchema.safeParse(validIndex).success).toBe(
       true,
@@ -150,17 +151,18 @@ describe('marketplace registry contract', () => {
       version: '2.0.0',
     });
     const index = parseMarketplaceRegistryIndex({
-      schemaVersion: 1,
+      schemaVersion: 3,
       entries: [
         ...(indexFor(bundle('1.0.0')).entries as unknown[]),
         ...(indexFor(bundle('2.0.0')).entries as unknown[]),
       ],
+      retirements: canonicalRetirements,
     });
     expect(
       resolveMarketplaceRegistryEntry(
         index,
         { id: 'community/registry-agent' },
-        { pluginVersion: '3.1.0', roleContractVersion: '1.0.0' },
+        { pluginVersion: '3.1.0' },
       ).version,
     ).toBe('2.0.0');
     expect(() =>
@@ -171,7 +173,7 @@ describe('marketplace registry contract', () => {
     ).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 second = indexFor(bundle('1.0.0', 'community/beta'));
     const entries = [
@@ -180,46 +182,46 @@ describe('marketplace registry contract', () => {
     ];
     expect(
       parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
+        schemaVersion: 3,
         entries,
-        retirements: [{ id: 'community/alpha' }],
+        retirements: [...canonicalRetirements, { id: 'community/alpha' }],
       }).schemaVersion,
-    ).toBe(2);
+    ).toBe(3);
     expect(() =>
       parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
+        schemaVersion: 3,
         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');
     expect(() =>
       parseMarketplaceRegistryIndex({
-        schemaVersion: 2,
+        schemaVersion: 3,
         entries,
-        retirements: [{ id: 'community/beta' }, { id: 'community/alpha' }],
+        retirements: [
+          ...canonicalRetirements,
+          { id: 'community/beta' },
+          { id: 'community/alpha' },
+        ],
       }),
     ).toThrow('sorted');
 
     const index = parseMarketplaceRegistryIndex({
-      schemaVersion: 2,
+      schemaVersion: 3,
       entries,
-      retirements: [{ id: 'community/alpha' }],
+      retirements: canonicalRetirements,
     });
     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(() =>
         resolveMarketplaceRegistryEntry(index, selector, {
           pluginVersion: '3.1.0',
-          roleContractVersion: '1.0.0',
         }),
       ).toThrow(MarketplaceRetiredError);
     }
@@ -265,14 +267,14 @@ describe('MarketplaceRegistryClient', () => {
         service.store.getLockfile().packages['community/registry-agent'].source,
       ).toEqual({
         kind: 'registry',
-        registry: 'https://registry.ohmyopencodeslim.com/v1/',
+        registry: 'https://registry.ohmyopencodeslim.com/v2/',
         indexUrl: MARKETPLACE_REGISTRY_INDEX_URL,
         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([
         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 {
       rmSync(root, { recursive: true, force: true });
@@ -372,7 +374,7 @@ describe('MarketplaceRegistryClient', () => {
     const oversized = new MarketplaceRegistryClient({
       pluginVersion: '3.1.0',
       maxIndexBytes: 10,
-      fetch: async () => response({ schemaVersion: 1, entries: [] }),
+      fetch: async () => response({ schemaVersion: 3, entries: [] }),
     });
     await expect(oversized.fetchIndex()).rejects.toBeInstanceOf(
       MarketplaceRegistryProtocolError,
@@ -433,12 +435,12 @@ describe('MarketplaceRegistryClient', () => {
   test('preserves the installed state after every remote validation failure', async () => {
     const interruptedBody = new ReadableStream<Uint8Array>({
       start(controller) {
-        controller.enqueue(new TextEncoder().encode('{"schemaVersion":1'));
+        controller.enqueue(new TextEncoder().encode('{"schemaVersion":3'));
         controller.error(new Error('connection interrupted'));
       },
     });
     const failures = [
-      async () => response({ schemaVersion: 1, entries: [] }),
+      async () => response({ schemaVersion: 3, entries: [] }),
       async (input: RequestInfo | URL) => {
         const index = indexFor(bundle());
         (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 {
   DEFAULT_MARKETPLACE_REGISTRY_URL,
+  isMarketplaceRegistryIdRetired,
   MarketplacePackageBundleSchema,
   type MarketplaceRegistryEntry,
   type MarketplaceRegistryIndex,
@@ -18,10 +19,7 @@ import {
   MarketplaceRetiredError,
 } from './errors';
 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 DEFAULT_MARKETPLACE_REGISTRY_TIMEOUT_MS = 10_000;
@@ -30,7 +28,6 @@ export const DEFAULT_MARKETPLACE_REGISTRY_ARTIFACT_MAX_BYTES = 512 * 1024;
 
 export interface MarketplaceRegistryClientOptions {
   pluginVersion: string;
-  roleContractVersion?: string;
   timeoutMs?: number;
   maxIndexBytes?: number;
   maxArtifactBytes?: number;
@@ -97,7 +94,6 @@ export class MarketplaceRegistryClient {
   private readonly timeoutMs: number;
   private readonly maxIndexBytes: number;
   private readonly maxArtifactBytes: number;
-  private readonly roleContractVersion: string;
 
   constructor(private readonly options: MarketplaceRegistryClientOptions) {
     this.fetcher = options.fetch ?? globalThis.fetch;
@@ -108,8 +104,6 @@ export class MarketplaceRegistryClient {
     this.maxArtifactBytes =
       options.maxArtifactBytes ??
       DEFAULT_MARKETPLACE_REGISTRY_ARTIFACT_MAX_BYTES;
-    this.roleContractVersion =
-      options.roleContractVersion ?? MARKETPLACE_ROLE_CONTRACT_VERSION;
   }
 
   async fetchIndex(signal?: AbortSignal): Promise<MarketplaceRegistryIndex> {
@@ -148,6 +142,11 @@ export class MarketplaceRegistryClient {
     })();
     assertMarketplacePackageNotRetired(selector.id);
     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);
     if (!matchingId) {
       throw new MarketplaceRegistryNotFoundError(
@@ -172,7 +171,6 @@ export class MarketplaceRegistryClient {
         selector,
         {
           pluginVersion: this.options.pluginVersion,
-          roleContractVersion: this.roleContractVersion,
         },
         minimumVersion,
       );
@@ -224,15 +222,9 @@ export class MarketplaceRegistryClient {
   }
 
   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 {
   MarketplaceAgentManifestSchema,
   MarketplacePackageManifestSchema,
-  MarketplaceProfileManifestSchema,
   MarketplaceVersionSchema,
 } from './schemas';
 
 const common = {
-  schemaVersion: 1 as const,
+  schemaVersion: 2 as const,
   id: 'community/example',
   version: '1.0.0',
   displayName: 'Example',
   description: 'An example package',
-  instructions: 'Follow these instructions.',
+  agentName: 'example',
+  prompt: 'Follow these instructions.',
   author: { name: 'Example Community' },
   tags: ['example'],
   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: {
     description: 'Explore example code.',
+    when: 'When exploration is needed.',
     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', () => {
     expect(MarketplaceVersionSchema.safeParse('v1.0.0').success).toBe(false);
     expect(
       MarketplacePackageManifestSchema.safeParse({
         ...common,
         version: 'v1.0.0',
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
       }).success,
     ).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(
       MarketplaceAgentManifestSchema.safeParse({
         ...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,
     ).toBe(true);
     expect(
       MarketplacePackageManifestSchema.safeParse({
         ...common,
-        kind: 'agent',
-        baseRole: 'orchestrator',
-        agentName: 'example',
-        overrides: {},
+        extends: {
+          builtin: 'explorer',
+          promptMode: 'append',
+          extra: true,
+        },
       }).success,
     ).toBe(false);
   });
 
-  test('rejects executable or non-canonical manifest fields', () => {
+  test('rejects extension-only builtin model policy for standalone agents', () => {
     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,
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: { promptSuffix: 'not canonical' },
+        model: { source: 'builtin' },
       }).success,
     ).toBe(false);
   });
 
-  test('rejects duplicate capabilities', () => {
+  test('rejects executable, duplicate, and unknown manifest fields', () => {
     expect(
       MarketplacePackageManifestSchema.safeParse({
         ...common,
-        capabilities: { tools: ['read', 'read'], permissions: [] },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        scripts: { install: 'rm -rf /' },
       }).success,
     ).toBe(false);
     expect(
       MarketplacePackageManifestSchema.safeParse({
         ...common,
-        capabilities: {
-          tools: [],
-          permissions: ['network.fetch', 'network.fetch'],
-        },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        id: 'Community/Example',
       }).success,
     ).toBe(false);
-  });
-
-  test('rejects overlapping required and optional requirements', () => {
     expect(
       MarketplacePackageManifestSchema.safeParse({
         ...common,
-        requirements: {
-          skills: { required: ['code-search'], optional: ['code-search'] },
-          mcps: { required: [], optional: [] },
-        },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        tools: ['read', 'read'],
       }).success,
     ).toBe(false);
     expect(
       MarketplacePackageManifestSchema.safeParse({
         ...common,
-        requirements: {
-          skills: { required: [], optional: [] },
-          mcps: { required: ['crawl4ai'], optional: ['crawl4ai'] },
-        },
-        kind: 'agent',
-        baseRole: 'explorer',
-        agentName: 'example',
-        overrides: {},
+        unknownField: true,
       }).success,
     ).toBe(false);
   });

+ 113 - 146
src/marketplace/schemas.ts

@@ -2,11 +2,11 @@ import { isAbsolute } from 'node:path';
 import { valid, validRange } from 'semver';
 import { z } from 'zod';
 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 =
   /^[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',
   );
 
-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 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
   .object({
@@ -42,88 +65,29 @@ export const MarketplaceAuthorSchema = z
   })
   .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({
-    required: UniqueBoundedStringArraySchema.default([]),
-    optional: UniqueBoundedStringArraySchema.default([]),
+    source: z.literal('explicit'),
+    candidates: z.array(MarketplaceModelCandidateSchema).min(1).max(16),
   })
   .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
   .object({
@@ -131,83 +95,87 @@ export const MarketplaceCompatibilitySchema = z
       (range) => validRange(range) !== null,
       'Expected a valid plugin semantic-version range',
     ),
-    roleContract: BoundedTextSchema(64).refine(
-      (range) => validRange(range) !== null,
-      'Expected a valid role-contract semantic-version range',
-    ),
   })
   .strict();
 
 export const MarketplaceRoutingSchema = z
   .object({
     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();
 
-export const MarketplaceAgentOverridesSchema = z
+export const MarketplaceExtensionSchema = z
   .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
-  .object({ manifest: MarketplacePackageManifestSchema })
+  .object({
+    manifest: MarketplacePackageManifestSchema,
+  })
   .strict();
 
 export const MarketplaceLocalSourceSchema = z
@@ -269,13 +237,12 @@ export const MarketplaceLockfileSchema = z
 
 export type MarketplacePackageId = z.infer<typeof MarketplacePackageIdSchema>;
 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<
   typeof MarketplaceAgentManifestSchema
 >;
-export type MarketplaceProfileManifest = z.infer<
-  typeof MarketplaceProfileManifestSchema
->;
 export type MarketplacePackageManifest = z.infer<
   typeof MarketplacePackageManifestSchema
 >;

+ 0 - 1
src/marketplace/service.ts

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

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

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

+ 3 - 30
src/marketplace/status.ts

@@ -19,7 +19,6 @@ export interface MarketplaceLivePackage {
   version: string;
   digest: string;
   runtimeName: string;
-  kind: 'agent' | 'profile';
 }
 
 export interface MarketplaceLiveSnapshot {
@@ -30,9 +29,7 @@ export interface MarketplaceLiveSnapshot {
 export interface MarketplaceInstalledSummary {
   id: string;
   version?: string;
-  kind?: 'agent' | 'profile';
   displayName?: string;
-  role?: string;
   digest?: string;
   valid: boolean;
 }
@@ -46,7 +43,6 @@ export interface MarketplaceStatusReport {
   configured: {
     preset?: string;
     agents: string[];
-    profiles: Record<string, string | null>;
   };
   live?: {
     packages: MarketplaceLivePackage[];
@@ -64,19 +60,11 @@ export interface CollectMarketplaceStatusOptions {
   desiredLive?: MarketplaceLiveSnapshot;
 }
 
-function packageRole(pkg: StoredMarketplacePackage): string {
-  return pkg.manifest.kind === 'agent'
-    ? pkg.manifest.baseRole
-    : pkg.manifest.targetRole;
-}
-
 function summarize(pkg: StoredMarketplacePackage): MarketplaceInstalledSummary {
   return {
     id: pkg.manifest.id,
     version: pkg.manifest.version,
-    kind: pkg.manifest.kind,
     displayName: pkg.manifest.displayName,
-    role: packageRole(pkg),
     digest: pkg.digest,
     valid: true,
   };
@@ -86,10 +74,7 @@ function configuredIds(
   activation: MarketplaceActivation | undefined,
 ): string[] {
   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: {
@@ -274,7 +259,6 @@ export function collectMarketplaceStatus(
     configured: {
       ...(preset ? { preset } : {}),
       agents: [...(activation?.agents ?? [])],
-      profiles: { ...(activation?.profiles ?? {}) },
     },
     ...(livePackages
       ? {
@@ -304,11 +288,9 @@ export function formatMarketplaceStatus(
   } else {
     for (const pkg of report.installed) {
       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 valid = pkg.valid ? '' : ' corrupt';
-      lines.push(`  ${pkg.id}${version}${kind}${role}${name}${valid}`);
+      lines.push(`  ${pkg.id}${version}${name}${valid}`);
     }
   }
   lines.push('', 'configured_agents:');
@@ -317,15 +299,6 @@ export function formatMarketplaceStatus(
   } else {
     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) {
     lines.push('', 'live_packages:');
     if (report.live.packages.length === 0) {
@@ -333,7 +306,7 @@ export function formatMarketplaceStatus(
     } else {
       for (const entry of report.live.packages) {
         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,
   mkdirSync,
   mkdtempSync,
+  readdirSync,
   readFileSync,
   rmSync,
   writeFileSync,
@@ -17,13 +18,11 @@ import {
   MarketplaceConflictError,
   MarketplaceIntegrityError,
   MarketplaceLockfileError,
-  MarketplaceLockOwnershipError,
   type MarketplacePackageBundle,
   MarketplaceRetiredError,
   MarketplaceService,
   MarketplaceStore,
 } from './index';
-import { heartbeatPath, publishGeneration, readLeaseState } from './lease';
 import type { MarketplacePaths } from './paths';
 
 function bundle(
@@ -32,36 +31,28 @@ function bundle(
 ): MarketplacePackageBundle {
   return {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/example',
       version,
-      kind: 'agent',
       displayName: 'Example',
       description: 'An example package',
-      instructions: 'Use the example role carefully.',
+      agentName: 'example',
+      prompt: 'Use the example role carefully.',
       author: { name: 'Example Community' },
       tags: ['example'],
       license: 'MIT',
       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: {
         description: 'Explore example code.',
         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,
     } as MarketplacePackageBundle['manifest'],
   };
@@ -71,18 +62,13 @@ function tempRoot(): string {
   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);
   fsModule.utimesSync(target, old, old);
-  fsModule.utimesSync(join(target, 'meta.json'), old, old);
-  fsModule.utimesSync(join(target, 'heartbeat'), old, old);
 }
 
 function spawnLockWorker(
@@ -98,7 +84,7 @@ function spawnLockWorker(
 ${body}`,
       JSON.stringify({ paths, logPath }),
     ],
-    { stdout: 'ignore', stderr: 'ignore' },
+    { stdout: 'ignore', stderr: 'pipe' },
   );
 }
 
@@ -131,12 +117,12 @@ describe('MarketplaceStore', () => {
         'community/example',
       ]);
       expect(store.getLockfile().packages['community/example']).toEqual({
-        manifestSchemaVersion: 1,
+        manifestSchemaVersion: 2,
         manifestVersion: '1.0.0',
         source: { kind: 'in-memory', label: 'programmatic' },
         digest: {
           algorithm: 'sha256',
-          domain: 'marketplace-bundle-v1',
+          domain: 'marketplace-agent-bundle-v2',
           value: installed.digest,
         },
       });
@@ -250,9 +236,7 @@ describe('MarketplaceStore', () => {
       const store = new MarketplaceStore({ rootDir: root });
       store.install(bundle());
       expect(() =>
-        store.install(
-          bundle('1.0.0', { instructions: 'Changed instructions.' }),
-        ),
+        store.install(bundle('1.0.0', { prompt: 'Changed instructions.' })),
       ).toThrow(MarketplaceConflictError);
     } finally {
       rmSync(root, { recursive: true, force: true });
@@ -327,12 +311,11 @@ describe('MarketplaceStore', () => {
         lock: { timeoutMs: 10, retryMs: 1 },
       });
       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);
     } finally {
       rmSync(root, { recursive: true, force: true });
@@ -344,10 +327,14 @@ describe('MarketplaceStore', () => {
     try {
       const store = new MarketplaceStore({
         rootDir: root,
-        lock: { staleMs: 200, heartbeatMs: 50, retryMs: 1 },
+        lock: { staleMs: 200, retryMs: 1 },
       });
       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');
     } finally {
       rmSync(root, { recursive: true, force: true });
@@ -359,7 +346,7 @@ describe('MarketplaceStore', () => {
     const readyPath = join(root, 'worker-ready');
     const store = new MarketplaceStore({
       rootDir: root,
-      lock: { staleMs: 100, heartbeatMs: 10, timeoutMs: 30, retryMs: 1 },
+      lock: { staleMs: 500, timeoutMs: 30, retryMs: 1 },
     });
     const worker = Bun.spawn(
       [
@@ -368,7 +355,7 @@ describe('MarketplaceStore', () => {
         `import { writeFileSync } from 'node:fs';
 import { acquireMarketplaceLeaseForPaths } from './src/marketplace/store.ts';
 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');
 await new Promise(() => {});`,
         JSON.stringify({ paths: store.paths, readyPath }),
@@ -387,7 +374,7 @@ await new Promise(() => {});`,
       expect(() => store.list()).toThrow(MarketplaceBusyError);
       worker.kill();
       await worker.exited;
-      await Bun.sleep(250);
+      await Bun.sleep(700);
       expect(store.list()).toEqual([]);
     } finally {
       worker.kill();
@@ -400,15 +387,18 @@ await new Promise(() => {});`,
     const root = tempRoot();
     const store = new MarketplaceStore({
       rootDir: root,
-      lock: { staleMs: 1000, heartbeatMs: 100, timeoutMs: 2000, retryMs: 5 },
+      lock: { staleMs: 1000, timeoutMs: 2000, retryMs: 5 },
     });
     const logPath = join(root, 'reclaim.log');
     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 { 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';
 try {
   lease.commit(() => {
@@ -429,6 +419,12 @@ try {
       const exitCodes = await Promise.all(
         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(readFileSync(logPath, 'utf8').trim().split('\n')).toEqual([
         'acquired',
@@ -436,44 +432,39 @@ try {
         'acquired',
         'released',
       ]);
-      expect(readLeaseState(store.paths.breakerDir)).toBeUndefined();
+      expect(existsSync(store.paths.lockDir)).toBe(true);
     } finally {
       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 store = new MarketplaceStore({
       rootDir: root,
-      lock: { staleMs: 200, heartbeatMs: 50, timeoutMs: 1000, retryMs: 10 },
+      lock: { staleMs: 1_000, timeoutMs: 1000, retryMs: 10 },
     });
     try {
       const owner = acquireMarketplaceLeaseForPaths(store.paths, {
-        staleMs: 200,
-        heartbeatMs: 50,
+        staleMs: 1_000,
         timeoutMs: 1000,
         retryMs: 10,
       });
       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 {
       rmSync(root, { recursive: true, force: true });
     }
@@ -631,7 +622,7 @@ describe('MarketplaceService', () => {
 
       expect(() =>
         service.install(
-          bundle('1.0.0', { instructions: 'A different package body.' }),
+          bundle('1.0.0', { prompt: 'A different package body.' }),
         ),
       ).toThrow(MarketplaceConflictError);
 

+ 0 - 1
src/marketplace/store.ts

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

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

@@ -33,78 +33,33 @@ function agentBundle(
 ): MarketplacePackageBundle {
   return {
     manifest: {
-      schemaVersion: 1,
+      schemaVersion: 2,
       id: 'community/docs-researcher',
       version,
-      kind: 'agent',
       displayName: 'Docs researcher',
       description: 'A derived explorer.',
-      instructions: 'Prefer documentation paths first.',
+      agentName: 'docsresearcher',
+      prompt: 'Prefer documentation paths first.',
       author: { name: 'Community' },
       tags: ['docs'],
       license: 'MIT',
       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: {
         description: 'Research docs and examples.',
         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,
     } 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(
   directory: string,
   bundle: MarketplacePackageBundle,
@@ -163,184 +118,132 @@ describe('marketplace tool args', () => {
     expect(() =>
       parseMarketplaceToolArgs({ action: 'list', path: './package.json' }),
     ).toThrow();
-    expect(() =>
-      parseMarketplaceToolArgs({
-        action: 'profile',
-        role: 'explorer',
-        packageId: 'community/deep-explorer',
-        clear: true,
-      }),
-    ).toThrow();
   });
 });
 
 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 () => {
     const { root, project, service, marketplace, context, live } =
@@ -359,7 +262,6 @@ describe('marketplace tool', () => {
           version: installed.manifest.version,
           digest: installed.digest,
           runtimeName: installed.manifest.agentName,
-          kind: 'agent',
         },
       ];
       const v2 = writeBundle(project, agentBundle('1.1.0'), 'package-v2.json');
@@ -392,7 +294,6 @@ describe('marketplace tool', () => {
           version: installed.manifest.version,
           digest: installed.digest,
           runtimeName: installed.manifest.agentName,
-          kind: 'agent',
         },
       ];
       const removed = String(
@@ -428,7 +329,6 @@ describe('marketplace tool', () => {
           version: installed.manifest.version,
           digest: installed.digest,
           runtimeName: installed.manifest.agentName,
-          kind: 'agent',
         },
       ];
       const again = String(

+ 2 - 69
src/tools/marketplace.ts

@@ -4,7 +4,6 @@ import { z } from 'zod';
 import {
   disableMarketplacePackage,
   enableMarketplaceAgent,
-  setMarketplaceProfile,
 } from '../marketplace/activation-config';
 import type { MarketplaceService } from '../marketplace/service';
 import {
@@ -26,7 +25,6 @@ export const MARKETPLACE_TOOL_ACTIONS = [
   'update',
   'enable',
   'disable',
-  'profile',
   'remove',
   'status',
 ] as const;
@@ -69,14 +67,6 @@ const MarketplaceToolRequestSchema = z.discriminatedUnion('action', [
       force: z.boolean().optional(),
     })
     .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('status') }).strict(),
 ]);
@@ -110,15 +100,6 @@ export function parseMarketplaceToolArgs(
     throw new Error(result.error.message);
   }
   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;
 }
 
@@ -145,22 +126,13 @@ function formatPackageLine(pkg: {
   manifest: {
     id: string;
     version: string;
-    kind: string;
     displayName: string;
     description: string;
-    baseRole?: string;
-    targetRole?: string;
   };
   source: unknown;
 }): string {
-  const role =
-    pkg.manifest.kind === 'agent'
-      ? pkg.manifest.baseRole
-      : pkg.manifest.targetRole;
   return [
     `${pkg.manifest.id}@${pkg.manifest.version}`,
-    `kind: ${pkg.manifest.kind}`,
-    `role: ${role}`,
     `displayName: ${pkg.manifest.displayName}`,
     `description: ${pkg.manifest.description}`,
     `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.
 
-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: {
       action: toolZ
         .enum(MARKETPLACE_TOOL_ACTIONS)
@@ -194,19 +166,10 @@ Action-specific fields: packageId for install/update/show/enable/disable/profile
         .describe(
           'Canonical package ID for registry install/update or local actions',
         ),
-      role: toolZ
-        .string()
-        .min(1)
-        .optional()
-        .describe('Specialist role for profile'),
       force: toolZ
         .boolean()
         .optional()
         .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
         .boolean()
         .optional()
@@ -277,11 +240,7 @@ Action-specific fields: packageId for install/update/show/enable/disable/profile
           return service
             .list()
             .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');
         case 'show':
@@ -309,32 +268,6 @@ Action-specific fields: packageId for install/update/show/enable/disable/profile
             options,
             `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':
           return formatMarketplaceStatus(
             collectMarketplaceStatus({

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

@@ -92,24 +92,6 @@ describe('switchPresetOnDisk', () => {
     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', () => {
     const config: PluginConfig = {
       presets: {

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

@@ -61,9 +61,7 @@ export function switchPresetOnDisk(
   }
 
   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) {
     return {
       ok: false,