Просмотр исходного кода

feat(marketplace): expose auto-delegation guidance

Alvin Unreal 4 дней назад
Родитель
Сommit
e43301d8f7

+ 7 - 2
docs/marketplace.md

@@ -88,5 +88,10 @@ Registry CI and static site tooling can import the narrow
 `oh-my-opencode-slim/marketplace-contract` package subpath. It provides the
 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.
+canonical bundle SHA-256 digest used by the plugin store. It also exports
+`renderDefaultMarketplaceAutoDelegationBlock(manifest)`, the authoritative
+default routing block for marketplace agents. Built-in extensions use the
+current built-in role routing plus the package's routing suffix; standalone
+packages use a generic lane block. This default renderer does not apply owner
+or runtime display-alias overrides. The public contract does not add
+root-package exports.

+ 1 - 1
package.json

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

+ 38 - 37
src/agents/index.ts

@@ -23,6 +23,7 @@ import {
   reservedRuntimeNames,
   resolveMarketplaceActivation,
 } from '../marketplace/activation';
+import { renderMarketplaceAutoDelegationBlock } from '../marketplace/routing';
 import { MARKETPLACE_TOOL_NAMES } from '../marketplace/schemas';
 import type {
   MarketplaceLivePackage,
@@ -1103,6 +1104,7 @@ export function isSubagent(name: string): name is SubagentName {
 function buildRoutingEntriesFromAgents(
   agents: readonly AgentDefinition[],
   guidanceByAgent: ReadonlyMap<string, string>,
+  marketplace?: MarketplaceActivationPlan,
 ): RoutingEntry[] {
   const entries = agents.flatMap((agent): RoutingEntry[] => {
     if (agent.name === 'council') {
@@ -1119,37 +1121,52 @@ function buildRoutingEntriesFromAgents(
     if (agent.name === 'councillor' || agent.name.startsWith('councillor-')) {
       return [];
     }
+    const marketplaceManifest = marketplace?.agents.find(
+      (entry) => entry.manifest.agentName === agent.name,
+    )?.manifest;
+    const runtimeName = agent.displayName
+      ? normalizeAgentName(agent.displayName)
+      : agent.name;
     if (agent.baseRole) {
-      const runtimeName = agent.displayName
-        ? normalizeAgentName(agent.displayName)
-        : agent.name;
-      const routingBlock = renderRoleRoutingBlock(
+      const roleRoutingBlock = renderRoleRoutingBlock(
         ROLE_DEFINITIONS[agent.baseRole],
         runtimeName,
       );
+      const routingBlock = marketplaceManifest
+        ? renderMarketplaceAutoDelegationBlock(marketplaceManifest, runtimeName)
+        : roleRoutingBlock;
       return [
         {
           agentName: runtimeName,
-          routingBlock: appendRoutingGuidance(
-            routingBlock,
-            guidanceByAgent.get(agent.name),
-          ),
+          routingBlock: guidanceByAgent.has(agent.name)
+            ? appendRoutingGuidance(
+                marketplaceManifest ? roleRoutingBlock : routingBlock,
+                guidanceByAgent.get(agent.name),
+              )
+            : routingBlock,
         },
       ];
     }
-    const runtimeName = agent.displayName
-      ? normalizeAgentName(agent.displayName)
-      : agent.name;
+    const genericRoutingBlock = [
+      `@${runtimeName}`,
+      `- Lane: ${agent.description ?? `Configured agent ${agent.name}`}`,
+    ].join('\n');
+    const routingBlock = marketplaceManifest
+      ? renderMarketplaceAutoDelegationBlock(
+          marketplaceManifest,
+          runtimeName,
+          agent.description,
+        )
+      : genericRoutingBlock;
     return [
       {
         agentName: runtimeName,
-        routingBlock: appendRoutingGuidance(
-          [
-            `@${runtimeName}`,
-            `- Lane: ${agent.description ?? `Configured agent ${agent.name}`}`,
-          ].join('\n'),
-          guidanceByAgent.get(agent.name),
-        ),
+        routingBlock: guidanceByAgent.has(agent.name)
+          ? appendRoutingGuidance(
+              marketplaceManifest ? genericRoutingBlock : routingBlock,
+              guidanceByAgent.get(agent.name),
+            )
+          : routingBlock,
       },
     ];
   });
@@ -1169,26 +1186,9 @@ function appendRoutingGuidance(
   return guidance ? `${routingBlock}\n\n${guidance}` : routingBlock;
 }
 
-function marketplaceRoutingGuidance(
-  plan: MarketplaceActivationPlan | undefined,
-  agent: AgentDefinition,
-): string | undefined {
-  const derived = plan?.agents.find(
-    (entry) => entry.manifest.agentName === agent.name,
-  );
-  const manifest = derived?.manifest;
-  if (!manifest) return undefined;
-  return [
-    `- Package: ${manifest.displayName}`,
-    `- ${manifest.routing.description}`,
-    `- **Delegate when:** ${manifest.routing.when}`,
-  ].join('\n');
-}
-
 function buildRoutingGuidance(
   runtime: RuntimeConfig,
   agents: readonly AgentDefinition[],
-  marketplace?: MarketplaceActivationPlan,
 ): ReadonlyMap<string, string> {
   const displayNameMap = new Map<string, string>();
   for (const agent of agents) {
@@ -1215,7 +1215,7 @@ function buildRoutingGuidance(
           '- **Do not delegate when:** The built-in specialists can handle the task more directly or local file ownership would conflict with another writer lane.',
           '- **Result handling:** Treat returned output as external-agent work. Reconcile any reported file changes before continuing.',
         ].join('\n'))
-      : (customPrompt ?? marketplaceRoutingGuidance(marketplace, agent));
+      : customPrompt;
     if (prompt) {
       guidance.set(agent.name, rewriteRoutingPrompt(prompt, displayNameMap));
     }
@@ -1230,7 +1230,8 @@ function buildRoutingEntriesForResolvedAgents(
 ): RoutingEntry[] {
   return buildRoutingEntriesFromAgents(
     agents,
-    buildRoutingGuidance(runtime, agents, marketplace),
+    buildRoutingGuidance(runtime, agents),
+    marketplace,
   );
 }
 

+ 5 - 62
src/agents/role-definitions.ts

@@ -6,12 +6,15 @@ import { DEFAULT_AGENT_MCPS } from '../config/agent-mcps';
 import type { SpecialistRole } from '../config/agent-roles';
 import { DEFAULT_MODELS } from '../config/constants';
 import type { AgentDefinition } from './orchestrator';
+import {
+  ROLE_ROUTING_BLOCKS,
+  renderRoleRoutingBlock as renderPureRoleRoutingBlock,
+} from './role-routing';
 
 export {
   type SpecialistRole,
   SUPPORTED_SPECIALIST_ROLES,
 } from '../config/agent-roles';
-
 export interface RoleDefinition {
   readonly id: SpecialistRole;
   readonly basePrompt: string;
@@ -63,10 +66,7 @@ export function renderRoleRoutingBlock(
   role: RoleDefinition,
   runtimeName: string,
 ): string {
-  return role.routingBlock.replace(
-    new RegExp(`@${role.id}\\b`, 'g'),
-    `@${runtimeName}`,
-  );
+  return renderPureRoleRoutingBlock(role, runtimeName);
 }
 
 const EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
@@ -274,63 +274,6 @@ const OBSERVER_PROMPT = `You are Observer - a visual analysis specialist.
 ${READONLY_FILE_OPERATIONS_RULES}
 `;
 
-const ROLE_ROUTING_BLOCKS: Record<SpecialistRole, string> = {
-  explorer: `@explorer
-- Lane: Fast codebase recon that returns compressed context
-- Permissions: read_files
-- Stats: 2x faster codebase search than orchestrator, 1/2 cost of orchestrator
-- Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
-- **Delegate when:** Need to discover what exists before planning • Parallel searches speed discovery • Need summarized map vs full contents • Broad/uncertain scope
-- **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
-  librarian: `@librarian
-- Lane: External knowledge and library research, fast web research
-- Role: Authoritative source for current library docs, API references, examples, bug investigations, and web retrieval
-- Stats: 2x faster web research than orchestrator, 1/2 cost of orchestrator
-- **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices • Working on fixing tricky bug or problem and need latest web research information
-- **Don't delegate when:** Standard usage you're confident • Simple stable APIs • General programming knowledge • Info already in conversation • Built-in language features
-- **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly. "How do others solve or workaround this tricky issue?" → @librarian.`,
-  oracle: `@oracle
-- Lane: Architecture, risk, debugging strategy, and review
-- Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
-- Permissions: read_files
-- Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
-- Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
-- **Delegate when:** Major architectural decisions with long-term impact • Problems persisting after 2+ fix attempts • High-risk multi-system refactors • Costly trade-offs (performance vs maintainability) • Complex debugging with unclear root cause • Security/scalability/data integrity decisions • Genuinely uncertain and cost of wrong choice is high • Code needs simplification or YAGNI scrutiny
-- **Review use:** @oracle is an escalation, not a default verification step. Request independent @oracle review only when its analysis is expected to materially reduce risk or uncertainty.
-- **Don't delegate when:** Routine decisions you're confident about • First bug fix attempt • Straightforward trade-offs • Tactical "how" vs strategic "should" • Time-sensitive good-enough decisions • Quick research/testing can answer
-- **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
-  designer: `@designer
-- Lane: UI/UX design, related edits, design polish and review
-- Permissions: read_files, write_files
-- Stats: 10x better UI/UX than orchestrator
-- Capabilities: Good design taste, visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge.
-- Owns visual and interaction quality: layout, hierarchy, spacing, motion, affordances, responsive behavior, and overall feel.
-- Weakness: copywriting. Ask @designer to use grounded, normal wording, then have orchestrator review/fix copy after design work without changing visual or interaction intent.
-- Avoid: "Let me ask @designer how it should look and implement yourself" → instead: "Let me ask @designer to design and implement the UI/UX changes for me"
-- **Delegate when:** User-facing interfaces needing polish • Responsive layouts • UX-critical components (forms, nav, dashboards) • Visual consistency systems • Animations/micro-interactions • Landing/marketing pages • Refining functional→delightful • Reviewing existing UI/UX quality
-- **Don't delegate when:** Backend/logic with no visual • Quick prototypes where design doesn't matter yet.`,
-  fixer: `@fixer
-- Lane: Bounded implementation and executioner
-- Role: Fast execution specialist for well-defined tasks
-- Permissions: read_files, write_files
-- Stats: 2x faster code edits, 1/2 cost of orchestrator
-- Weakness: design, taste
-- Tools/Constraints: Execution-focused-no research, no architectural decisions
-- **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer • Parallelization benefits: Task involves multiple folders and multiple files modification, scoping work per folder and spawning parallel @fixer instances for each folder.
-- **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to @fixer > doing • Tight integration with your current work • Requires design taste, visual hierarchy, interaction polish, responsive layout decisions, animation/motion, component feel, or UI copy/design trade-offs
-- **Rule of thumb:** Headless/mechanical implementation → @fixer. User-visible design or polish → @designer. If @designer already set direction, @fixer may only do bounded mechanical follow-up that preserves that design exactly.`,
-  observer: `@observer
-- Lane: Visual/media analysis isolated from orchestrator context
-- Role: Visual analysis specialist for images, PDFs, and diagrams
-- Permissions: Read files
-- Stats: Saves main context tokens - @observer processes raw files, returns structured observations
-- Capabilities: Interprets images, screenshots, PDFs, and diagrams via native read tool; extracts UI elements, layouts, text, relationships
-- **Delegate when:** Need to analyze a multimedia file• Extract information
-- **Don't delegate when:** Plain text files that Read can handle directly • Files that need editing afterward (need literal content from Read)
-- **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer - it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for routing? → Read only the minimal context yourself.
-- **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png - describe the UI elements and error messages."`,
-};
-
 function defineRole(
   id: SpecialistRole,
   basePrompt: string,

+ 74 - 0
src/agents/role-routing.ts

@@ -0,0 +1,74 @@
+import type { SpecialistRole } from '../config/agent-roles';
+
+export interface RoleRoutingDefinition {
+  readonly id: SpecialistRole;
+  readonly routingBlock: string;
+}
+
+export const ROLE_ROUTING_BLOCKS: Readonly<Record<SpecialistRole, string>> =
+  Object.freeze({
+    explorer: `@explorer
+- Lane: Fast codebase recon that returns compressed context
+- Permissions: read_files
+- Stats: 2x faster codebase search than orchestrator, 1/2 cost of orchestrator
+- Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
+- **Delegate when:** Need to discover what exists before planning • Parallel searches speed discovery • Need summarized map vs full contents • Broad/uncertain scope
+- **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
+    librarian: `@librarian
+- Lane: External knowledge and library research, fast web research
+- Role: Authoritative source for current library docs, API references, examples, bug investigations, and web retrieval
+- Stats: 2x faster web research than orchestrator, 1/2 cost of orchestrator
+- **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices • Working on fixing tricky bug or problem and need latest web research information
+- **Don't delegate when:** Standard usage you're confident • Simple stable APIs • General programming knowledge • Info already in conversation • Built-in language features
+- **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly. "How do others solve or workaround this tricky issue?" → @librarian.`,
+    oracle: `@oracle
+- Lane: Architecture, risk, debugging strategy, and review
+- Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
+- Permissions: read_files
+- Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
+- Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
+- **Delegate when:** Major architectural decisions with long-term impact • Problems persisting after 2+ fix attempts • High-risk multi-system refactors • Costly trade-offs (performance vs maintainability) • Complex debugging with unclear root cause • Security/scalability/data integrity decisions • Genuinely uncertain and cost of wrong choice is high • Code needs simplification or YAGNI scrutiny
+- **Review use:** @oracle is an escalation, not a default verification step. Request independent @oracle review only when its analysis is expected to materially reduce risk or uncertainty.
+- **Don't delegate when:** Routine decisions you're confident about • First bug fix attempt • Straightforward trade-offs • Tactical "how" vs strategic "should" • Time-sensitive good-enough decisions • Quick research/testing can answer
+- **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
+    designer: `@designer
+- Lane: UI/UX design, related edits, design polish and review
+- Permissions: read_files, write_files
+- Stats: 10x better UI/UX than orchestrator
+- Capabilities: Good design taste, visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge.
+- Owns visual and interaction quality: layout, hierarchy, spacing, motion, affordances, responsive behavior, and overall feel.
+- Weakness: copywriting. Ask @designer to use grounded, normal wording, then have orchestrator review/fix copy after design work without changing visual or interaction intent.
+- Avoid: "Let me ask @designer how it should look and implement yourself" → instead: "Let me ask @designer to design and implement the UI/UX changes for me"
+- **Delegate when:** User-facing interfaces needing polish • Responsive layouts • UX-critical components (forms, nav, dashboards) • Visual consistency systems • Animations/micro-interactions • Landing/marketing pages • Refining functional→delightful • Reviewing existing UI/UX quality
+- **Don't delegate when:** Backend/logic with no visual • Quick prototypes where design doesn't matter yet.`,
+    fixer: `@fixer
+- Lane: Bounded implementation and executioner
+- Role: Fast execution specialist for well-defined tasks
+- Permissions: read_files, write_files
+- Stats: 2x faster code edits, 1/2 cost of orchestrator
+- Weakness: design, taste
+- Tools/Constraints: Execution-focused-no research, no architectural decisions
+- **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer • Parallelization benefits: Task involves multiple folders and multiple files modification, scoping work per folder and spawning parallel @fixer instances for each folder.
+- **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to @fixer > doing • Tight integration with your current work • Requires design taste, visual hierarchy, interaction polish, responsive layout decisions, animation/motion, component feel, or UI copy/design trade-offs
+- **Rule of thumb:** Headless/mechanical implementation → @fixer. User-visible design or polish → @designer. If @designer already set direction, @fixer may only do bounded mechanical follow-up that preserves that design exactly.`,
+    observer: `@observer
+- Lane: Visual/media analysis isolated from orchestrator context
+- Role: Visual analysis specialist for images, PDFs, and diagrams
+- Permissions: Read files
+- Stats: Saves main context tokens - @observer processes raw files, returns structured observations
+- Capabilities: Interprets images, screenshots, PDFs, and diagrams via native read tool; extracts UI elements, layouts, text, relationships
+- **Delegate when:** Need to analyze a multimedia file• Extract information
+- **Don't delegate when:** Plain text files that Read can handle directly • Files that need editing afterward (need literal content from Read)
+- **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer - it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for routing? → Read only the minimal context yourself.
+- **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png - describe the UI elements and error messages."`,
+  });
+
+export function renderRoleRoutingBlock(
+  role: RoleRoutingDefinition,
+  runtimeName: string,
+): string {
+  return role.routingBlock.replace(
+    new RegExp(`@${role.id}\\b`, 'g'),
+    `@${runtimeName}`,
+  );
+}

+ 41 - 0
src/marketplace-contract/index.test.ts

@@ -0,0 +1,41 @@
+import { describe, expect, test } from 'bun:test';
+import { ROLE_DEFINITIONS } from '../agents/role-definitions';
+import type { MarketplacePackageManifest } from '../marketplace/schemas';
+import { renderDefaultMarketplaceAutoDelegationBlock } from './index';
+
+const manifest: MarketplacePackageManifest = {
+  schemaVersion: 2,
+  id: 'community/contract-agent',
+  version: '1.0.0',
+  displayName: 'Contract agent',
+  description: 'A contract test agent.',
+  agentName: 'contract-agent',
+  prompt: 'Package prompt.',
+  routing: {
+    description: 'Contract routing description.',
+    when: 'The contract task matches.',
+    keywords: ['contract'],
+  },
+  skills: [],
+  mcps: [],
+  tools: [],
+  author: { name: 'Community' },
+  tags: ['contract'],
+  license: 'MIT',
+  compatibility: { plugin: '>=3.0.0-beta.3 <4.0.0' },
+  model: { source: 'explicit', candidates: ['provider/model'] },
+  extends: { builtin: 'explorer', promptMode: 'append' },
+};
+
+describe('marketplace contract routing export', () => {
+  test('exports the same authoritative default extension block', () => {
+    const expected = `${ROLE_DEFINITIONS.explorer.routingBlock.replaceAll(
+      '@explorer',
+      '@contract-agent',
+    )}\n\n- Package: Contract agent\n- Contract routing description.\n- **Delegate when:** The contract task matches.`;
+
+    expect(renderDefaultMarketplaceAutoDelegationBlock(manifest)).toBe(
+      expected,
+    );
+  });
+});

+ 12 - 0
src/marketplace-contract/index.ts

@@ -8,6 +8,7 @@ import {
 } from '../marketplace/canonical';
 import { MarketplaceRetiredError } from '../marketplace/errors';
 import { isMarketplacePackageRetired } from '../marketplace/retirements';
+import { renderMarketplaceAutoDelegationBlock } from '../marketplace/routing';
 import {
   MARKETPLACE_DIGEST_DOMAIN,
   MARKETPLACE_MANIFEST_SCHEMA_VERSION,
@@ -47,6 +48,17 @@ export {
   MarketplaceVersionSchema,
 };
 
+/**
+ * Render the authoritative default auto-delegation block for a marketplace
+ * manifest. Owner-configured orchestrator prompts and runtime display aliases
+ * are runtime concerns and are intentionally not represented here.
+ */
+export function renderDefaultMarketplaceAutoDelegationBlock(
+  manifest: MarketplacePackageManifest,
+): string {
+  return renderMarketplaceAutoDelegationBlock(manifest);
+}
+
 export const MARKETPLACE_REGISTRY_SCHEMA_VERSION = 3 as const;
 export const DEFAULT_MARKETPLACE_REGISTRY_URL =
   'https://registry.ohmyopencodeslim.com/v2/' as const;

+ 241 - 0
src/marketplace/routing.test.ts

@@ -0,0 +1,241 @@
+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 {
+  ROLE_DEFINITIONS,
+  SUPPORTED_SPECIALIST_ROLES,
+} from '../agents/role-definitions';
+import { RuntimeConfig } from '../config/runtime';
+import { renderDefaultMarketplaceAutoDelegationBlock } from '../marketplace-contract';
+import { renderMarketplaceAutoDelegationBlock } from './routing';
+import type { MarketplacePackageManifest } from './schemas';
+import { MarketplaceStore } from './store';
+
+const baseManifest: MarketplacePackageManifest = {
+  schemaVersion: 2,
+  id: 'community/routing-agent',
+  version: '1.0.0',
+  displayName: 'Routing agent',
+  description: 'A standalone routing agent.',
+  agentName: 'routing-agent',
+  prompt: 'Package prompt.',
+  routing: {
+    description: 'Package routing description.',
+    when: 'The task matches this package.',
+    keywords: ['routing'],
+  },
+  skills: [],
+  mcps: [],
+  tools: [],
+  author: { name: 'Community' },
+  tags: ['routing'],
+  license: 'MIT',
+  compatibility: { plugin: '>=3.0.0-beta.3 <4.0.0' },
+  model: { source: 'explicit', candidates: ['provider/model'] },
+};
+
+function expectedSuffix(manifest: MarketplacePackageManifest): string {
+  return [
+    `- Package: ${manifest.displayName}`,
+    `- ${manifest.routing.description}`,
+    `- **Delegate when:** ${manifest.routing.when}`,
+  ].join('\n');
+}
+
+describe('marketplace routing renderer', () => {
+  test('renders every supported extension role exactly', () => {
+    for (const roleName of SUPPORTED_SPECIALIST_ROLES) {
+      const manifest = {
+        ...baseManifest,
+        extends: { builtin: roleName, promptMode: 'append' },
+      } as MarketplacePackageManifest;
+      const expected = `${ROLE_DEFINITIONS[roleName].routingBlock.replaceAll(
+        `@${roleName}`,
+        `@${manifest.agentName}`,
+      )}\n\n${expectedSuffix(manifest)}`;
+
+      expect(renderMarketplaceAutoDelegationBlock(manifest)).toBe(expected);
+    }
+  });
+
+  test('does not vary routing for append versus replace prompt modes', () => {
+    for (const promptMode of ['append', 'replace'] as const) {
+      const manifest = {
+        ...baseManifest,
+        extends: { builtin: 'explorer', promptMode },
+      } as MarketplacePackageManifest;
+      const expected = `${ROLE_DEFINITIONS.explorer.routingBlock.replaceAll(
+        '@explorer',
+        `@${manifest.agentName}`,
+      )}\n\n${expectedSuffix(manifest)}`;
+
+      expect(renderMarketplaceAutoDelegationBlock(manifest)).toBe(expected);
+    }
+  });
+
+  test('renders standalone packages with the generic routing block', () => {
+    const expected = [
+      '@routing-agent',
+      '- Lane: A standalone routing agent.',
+      '',
+      expectedSuffix(baseManifest),
+    ].join('\n');
+
+    expect(renderMarketplaceAutoDelegationBlock(baseManifest)).toBe(expected);
+  });
+
+  test('renders a runtime display alias without changing the manifest default', () => {
+    const derived = {
+      ...baseManifest,
+      extends: { builtin: 'fixer', promptMode: 'append' as const },
+    };
+
+    expect(renderMarketplaceAutoDelegationBlock(derived, 'build-agent')).toBe(
+      `${ROLE_DEFINITIONS.fixer.routingBlock.replaceAll(
+        '@fixer',
+        '@build-agent',
+      )}\n\n${expectedSuffix(derived)}`,
+    );
+    expect(renderMarketplaceAutoDelegationBlock(derived)).toContain(
+      '@routing-agent',
+    );
+  });
+
+  test('uses the shared default renderer in the resolved routing registry', () => {
+    const root = mkdtempSync(join(tmpdir(), 'marketplace-routing-'));
+    try {
+      const manifest = {
+        ...baseManifest,
+        extends: { builtin: 'oracle', promptMode: 'replace' as const },
+      };
+      const store = new MarketplaceStore({ rootDir: root });
+      store.install({ manifest });
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: { 'routing-agent': { displayName: 'build-agent' } },
+            marketplace: { agents: ['community/routing-agent'] },
+          },
+        },
+      });
+      const registry = buildResolvedAgentRegistry(runtime, {
+        marketplaceStore: store,
+        availableMcpNames: [],
+      });
+      const route = registry.routing.find(
+        (entry) => entry.agentName === 'build-agent',
+      );
+
+      expect(route?.routingBlock).toBe(
+        renderMarketplaceAutoDelegationBlock(manifest, 'build-agent'),
+      );
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+
+  test('keeps custom orchestrator guidance and runtime aliases authoritative', () => {
+    const root = mkdtempSync(join(tmpdir(), 'marketplace-routing-custom-'));
+    try {
+      const manifest = {
+        ...baseManifest,
+        extends: { builtin: 'oracle', promptMode: 'append' as const },
+      };
+      const store = new MarketplaceStore({ rootDir: root });
+      store.install({ manifest });
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: {
+              'routing-agent': {
+                displayName: 'build-agent',
+                orchestratorPrompt: 'Use @routing-agent for this task.',
+              },
+            },
+            marketplace: { agents: ['community/routing-agent'] },
+          },
+        },
+      });
+      const registry = buildResolvedAgentRegistry(runtime, {
+        marketplaceStore: store,
+        availableMcpNames: [],
+      });
+      const route = registry.routing.find(
+        (entry) => entry.agentName === 'build-agent',
+      );
+      const roleRouting = ROLE_DEFINITIONS.oracle.routingBlock.replaceAll(
+        '@oracle',
+        '@build-agent',
+      );
+
+      expect(route?.routingBlock).toBe(
+        `${roleRouting}\n\nUse @build-agent for this task.`,
+      );
+      expect(route?.routingBlock).not.toContain('- Package:');
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+
+  test('keeps an owner description in standalone runtime routing only', () => {
+    const root = mkdtempSync(
+      join(tmpdir(), 'marketplace-routing-description-'),
+    );
+    try {
+      const store = new MarketplaceStore({ rootDir: root });
+      store.install({ manifest: baseManifest });
+      RuntimeConfig.reset(root);
+      const runtime = RuntimeConfig.init(root, {
+        preset: 'work',
+        presets: {
+          work: {
+            agents: {
+              'routing-agent': {
+                description: 'Owner-selected standalone lane.',
+              },
+            },
+            marketplace: { agents: ['community/routing-agent'] },
+          },
+        },
+      });
+      const registry = buildResolvedAgentRegistry(runtime, {
+        marketplaceStore: store,
+        availableMcpNames: [],
+      });
+      const runtimeBlock = [
+        '@routing-agent',
+        '- Lane: Owner-selected standalone lane.',
+        '',
+        expectedSuffix(baseManifest),
+      ].join('\n');
+      const publicBlock =
+        renderDefaultMarketplaceAutoDelegationBlock(baseManifest);
+      const route = registry.routing.find(
+        (entry) => entry.agentName === 'routing-agent',
+      );
+      const orchestrator = registry.agents.find(
+        (agent) => agent.name === 'orchestrator',
+      );
+
+      expect(route?.routingBlock).toBe(runtimeBlock);
+      expect(orchestrator?.config.prompt).toContain(runtimeBlock);
+      expect(publicBlock).toBe(
+        [
+          '@routing-agent',
+          '- Lane: A standalone routing agent.',
+          '',
+          expectedSuffix(baseManifest),
+        ].join('\n'),
+      );
+      expect(publicBlock).not.toContain('Owner-selected standalone lane.');
+    } finally {
+      rmSync(root, { recursive: true, force: true });
+    }
+  });
+});

+ 40 - 0
src/marketplace/routing.ts

@@ -0,0 +1,40 @@
+import {
+  ROLE_ROUTING_BLOCKS,
+  renderRoleRoutingBlock,
+} from '../agents/role-routing';
+import type { MarketplacePackageManifest } from './schemas';
+
+function renderMarketplacePackageSuffix(
+  manifest: MarketplacePackageManifest,
+): string {
+  return [
+    `- Package: ${manifest.displayName}`,
+    `- ${manifest.routing.description}`,
+    `- **Delegate when:** ${manifest.routing.when}`,
+  ].join('\n');
+}
+
+/**
+ * Render the default marketplace routing block for a runtime agent name.
+ *
+ * The default is deliberately independent of owner prompt overrides. Runtime
+ * routing may provide a display alias, while the public contract uses the
+ * manifest's canonical agentName by leaving runtimeName unset.
+ */
+export function renderMarketplaceAutoDelegationBlock(
+  manifest: MarketplacePackageManifest,
+  runtimeName = manifest.agentName,
+  standaloneLaneDescription = manifest.description,
+): string {
+  const role = manifest.extends
+    ? {
+        id: manifest.extends.builtin,
+        routingBlock: ROLE_ROUTING_BLOCKS[manifest.extends.builtin],
+      }
+    : undefined;
+  const base = role
+    ? renderRoleRoutingBlock(role, runtimeName)
+    : [`@${runtimeName}`, `- Lane: ${standaloneLaneDescription}`].join('\n');
+
+  return `${base}\n\n${renderMarketplacePackageSuffix(manifest)}`;
+}

+ 2 - 1
src/marketplace/store.test.ts

@@ -7,6 +7,7 @@ import {
   mkdtempSync,
   readdirSync,
   readFileSync,
+  realpathSync,
   rmSync,
   writeFileSync,
 } from 'node:fs';
@@ -652,7 +653,7 @@ describe('MarketplaceService', () => {
       service.installFile(packageFile);
       expect(service.show('community/example').source).toEqual({
         kind: 'local',
-        path: packageFile,
+        path: realpathSync(packageFile),
       });
     } finally {
       rmSync(root, { recursive: true, force: true });