{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "skills-agent",
  "title": "Skills Agent",
  "description": "A repo-local skills workspace that loads real SKILL.md packages on demand and turns rough ideas into durable agent assets.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@phosphor-icons/react",
    "@vercel/sandbox",
    "ai",
    "class-variance-authority",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json",
    "https://elements.ai-sdk.dev/api/registry/reasoning.json",
    "https://elements.ai-sdk.dev/api/registry/shimmer.json",
    "https://elements.ai-sdk.dev/api/registry/tool.json"
  ],
  "files": [
    {
      "path": "registry/skills-agent/app/demos/skills-agent/page.tsx",
      "content": "import { SkillsAgentScreen } from \"@/components/skills-agent/skills-agent-screen\";\n\nexport default function SkillsAgentPage() {\n  return <SkillsAgentScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/skills-agent/page.tsx"
    },
    {
      "path": "registry/skills-agent/app/api/demos/skills-agent/route.ts",
      "content": "import { handleSkillsAgentRequest } from \"@/lib/skills-agent/server/request\";\n\nexport const runtime = \"nodejs\";\n\nexport function POST(request: Request) {\n  return handleSkillsAgentRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/skills-agent/route.ts"
    },
    {
      "path": "registry/skills-agent/components/skills-agent/skills-agent-screen.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { ArrowLeft } from \"lucide-react\";\n\nimport { getSkillsAgentRuntimeState } from \"@/lib/skills-agent/server/runtime\";\nimport { SkillsAgentWorkspace } from \"./skills-agent-workspace\";\n\nexport async function SkillsAgentScreen() {\n  const runtimeState = await getSkillsAgentRuntimeState();\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6\">\n        <header className=\"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\">\n          <div className=\"space-y-2\">\n            <Breadcrumb>\n              <BreadcrumbList className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                <BreadcrumbItem>\n                  <BreadcrumbLink\n                    aria-label=\"Back to demos\"\n                    className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n                    href=\"/\"\n                  >\n                    <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                    <span>Demo</span>\n                  </BreadcrumbLink>\n                </BreadcrumbItem>\n                <BreadcrumbSeparator className=\"text-muted-foreground\">\n                  /\n                </BreadcrumbSeparator>\n                <BreadcrumbItem>\n                  <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n                    Skills Builder Agent\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              Load repo-local skills on demand and turn rough ideas into durable\n              agent assets\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This workspace tracks the AI SDK skills guide closely: the model\n              sees a lightweight catalog first, loads full skill instructions\n              only when needed, and executes inside an isolated workspace.\n            </p>\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <Badge variant=\"outline\">{runtimeState.statusLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.sandboxProvider}</Badge>\n            <Badge variant=\"outline\">{runtimeState.chatModel}</Badge>\n          </div>\n        </header>\n\n        <div className=\"lg:h-svh\">\n          <SkillsAgentWorkspace\n            availableSkills={runtimeState.availableSkills}\n            chatModel={runtimeState.chatModel}\n            environmentLabel={runtimeState.environmentLabel}\n            isChatAvailable={runtimeState.isChatAvailable}\n            sandboxProvider={runtimeState.sandboxProvider}\n            setupMessage={runtimeState.setupMessage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/skills-agent/skills-agent-screen.tsx"
    },
    {
      "path": "registry/skills-agent/components/skills-agent/skills-agent-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowClockwiseIcon,\n  HammerIcon,\n  StopIcon,\n  WrenchIcon,\n} from \"@phosphor-icons/react\";\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/components/ai-elements/reasoning\";\nimport { Shimmer } from \"@/components/ai-elements/shimmer\";\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n  type ToolPart,\n} from \"@/components/ai-elements/tool\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { isReasoningUIPart, isToolUIPart, type UIMessage } from \"ai\";\nimport { useMemo } from \"react\";\nimport { useSkillsAgentChat } from \"./use-skills-agent-chat\";\n\nconst configuredTools = [\n  {\n    description: \"Load full skill instructions from the configured skills set.\",\n    name: \"skill\",\n  },\n  {\n    description: \"Run shell commands inside the session sandbox.\",\n    name: \"bash\",\n  },\n  {\n    description: \"Read sandbox-backed files directly.\",\n    name: \"readFile\",\n  },\n  {\n    description: \"Write sandbox-backed files directly.\",\n    name: \"writeFile\",\n  },\n] as const;\n\nfunction getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nfunction getReasoningText(message: UIMessage) {\n  return message.parts\n    .filter(isReasoningUIPart)\n    .map((part) => part.text)\n    .join(\"\\n\\n\");\n}\n\nfunction getToolParts(message: UIMessage) {\n  return message.parts.filter(isToolUIPart);\n}\n\nfunction getToolName(part: ToolPart) {\n  return part.type === \"dynamic-tool\"\n    ? part.toolName\n    : part.type.split(\"-\").slice(1).join(\"-\");\n}\n\nfunction readObjectField(value: unknown, field: string): string | null {\n  if (!value || typeof value !== \"object\" || !(field in value)) {\n    return null;\n  }\n\n  const candidate = value[field as keyof typeof value];\n\n  return typeof candidate === \"string\" ? candidate : null;\n}\n\nfunction getActivatedSkills(toolParts: ToolPart[]) {\n  const skillNames = toolParts\n    .filter(\n      (part) =>\n        getToolName(part) === \"skill\" && part.state === \"output-available\"\n    )\n    .map((part) => {\n      const nestedSkill =\n        part.output && typeof part.output === \"object\" && \"skill\" in part.output\n          ? part.output.skill\n          : null;\n\n      return (\n        readObjectField(nestedSkill, \"name\") ??\n        readObjectField(part.output, \"skillName\")\n      );\n    })\n    .filter((name): name is string => Boolean(name));\n\n  return Array.from(new Set(skillNames));\n}\n\nfunction getDraftArtifacts(toolParts: ToolPart[]) {\n  return toolParts\n    .filter(\n      (part) =>\n        getToolName(part) === \"writeFile\" && part.state === \"output-available\"\n    )\n    .map((part) => {\n      const inputPath = readObjectField(part.input, \"path\");\n      const inputContent = readObjectField(part.input, \"content\");\n      const outputPath = readObjectField(part.output, \"path\");\n\n      return {\n        content: inputContent,\n        path: outputPath ?? inputPath,\n      };\n    })\n    .filter((artifact) => artifact.path && artifact.content);\n}\n\ninterface AssistantTraceProps {\n  isLastMessage: boolean;\n  isStreaming: boolean;\n  message: UIMessage;\n}\n\nfunction AssistantTrace({\n  isLastMessage,\n  isStreaming,\n  message,\n}: AssistantTraceProps) {\n  const text = getTextContent(message);\n  const reasoningText = getReasoningText(message);\n  const toolParts = getToolParts(message);\n  const activatedSkills = getActivatedSkills(toolParts);\n  const draftArtifacts = getDraftArtifacts(toolParts);\n  const hasReasoning = reasoningText.length > 0;\n  const hasText = text.length > 0;\n  const lastPart = message.parts.at(-1);\n  const isReasoningStreaming =\n    isLastMessage && isStreaming && lastPart?.type === \"reasoning\";\n  const showBodyThinking =\n    isLastMessage && isStreaming && !hasReasoning && !hasText;\n  let bodyContent: React.ReactNode = null;\n\n  if (hasText) {\n    bodyContent = <MessageResponse>{text}</MessageResponse>;\n  } else if (showBodyThinking) {\n    bodyContent = <Shimmer className=\"text-sm\">Thinking...</Shimmer>;\n  }\n\n  return (\n    <>\n      {hasReasoning ? (\n        <Reasoning className=\"w-full\" isStreaming={isReasoningStreaming}>\n          <ReasoningTrigger />\n          <ReasoningContent>{reasoningText}</ReasoningContent>\n        </Reasoning>\n      ) : null}\n\n      {activatedSkills.length > 0 ? (\n        <div className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n            Activated skills\n          </p>\n          <div className=\"flex flex-wrap gap-2\">\n            {activatedSkills.map((skillName) => (\n              <Badge key={skillName} variant=\"secondary\">\n                {skillName}\n              </Badge>\n            ))}\n          </div>\n        </div>\n      ) : null}\n\n      {draftArtifacts.length > 0 ? (\n        <div className=\"space-y-3\">\n          {draftArtifacts.map((artifact) => (\n            <div\n              className=\"space-y-2 border border-foreground/10 p-3\"\n              key={`${artifact.path}-${artifact.content?.length ?? 0}`}\n            >\n              <div className=\"flex items-center justify-between gap-3\">\n                <p className=\"font-medium text-sm\">Draft artifact</p>\n                <Badge variant=\"outline\">{artifact.path}</Badge>\n              </div>\n              <pre className=\"overflow-x-auto whitespace-pre-wrap text-xs/relaxed\">\n                {artifact.content}\n              </pre>\n            </div>\n          ))}\n        </div>\n      ) : null}\n\n      {toolParts.map((part) => (\n        <Tool key={part.toolCallId}>\n          {part.type === \"dynamic-tool\" ? (\n            <ToolHeader\n              state={part.state}\n              toolName={part.toolName}\n              type={part.type}\n            />\n          ) : (\n            <ToolHeader state={part.state} type={part.type} />\n          )}\n          <ToolContent>\n            {part.input ? <ToolInput input={part.input} /> : null}\n            <ToolOutput errorText={part.errorText} output={part.output} />\n          </ToolContent>\n        </Tool>\n      ))}\n\n      {bodyContent}\n    </>\n  );\n}\n\nexport interface SkillsAgentWorkspaceProps {\n  availableSkills: Array<{\n    description: string;\n    name: string;\n  }>;\n  chatModel: string;\n  environmentLabel: string;\n  isChatAvailable: boolean;\n  sandboxProvider: string;\n  setupMessage: string | null;\n}\n\nexport function SkillsAgentWorkspace({\n  availableSkills,\n  chatModel,\n  environmentLabel,\n  isChatAvailable,\n  sandboxProvider,\n  setupMessage,\n}: SkillsAgentWorkspaceProps) {\n  const {\n    error,\n    hasMessages,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    status,\n    stop,\n  } = useSkillsAgentChat();\n  const samplePrompts = useMemo(\n    () => [\n      \"Grill this rough idea for a docs chatbot.\",\n      \"Interrogate this agent concept until the glossary and boundaries are precise.\",\n      \"After the context is aligned, draft a reusable SKILL.md package for the workflow.\",\n    ],\n    []\n  );\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_22rem]\">\n      <section className=\"flex min-h-[70svh] flex-col border border-foreground/10 bg-background lg:h-full lg:min-h-0\">\n        {isChatAvailable ? null : (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-muted-foreground text-xs/relaxed\">\n            {setupMessage}\n          </div>\n        )}\n\n        {error ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {error.message}\n          </div>\n        ) : null}\n\n        <Conversation className=\"min-h-0\">\n          <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n            {hasMessages ? (\n              messages.map((message, index) => (\n                <Message from={message.role} key={message.id}>\n                  <MessageContent\n                    className={cn(\n                      \"space-y-4\",\n                      message.role === \"assistant\" ? \"max-w-3xl\" : \"max-w-2xl\"\n                    )}\n                  >\n                    {message.role === \"assistant\" ? (\n                      <AssistantTrace\n                        isLastMessage={index === messages.length - 1}\n                        isStreaming={isBusy}\n                        message={message}\n                      />\n                    ) : (\n                      <MessageResponse>\n                        {getTextContent(message)}\n                      </MessageResponse>\n                    )}\n                  </MessageContent>\n                </Message>\n              ))\n            ) : (\n              <ConversationEmptyState\n                description=\"Ask the agent to challenge a rough idea, align durable context, and turn that workflow into a reusable skill draft.\"\n                icon={<HammerIcon className=\"size-5\" />}\n                title=\"Idea-to-skill workspace is ready\"\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <div className=\"border-foreground/10 border-t px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-3xl\">\n            <PromptInput onSubmit={({ text }) => sendMessage({ text })}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!isChatAvailable || isBusy}\n                  placeholder=\"Describe a rough idea or ask for a reusable skill draft.\"\n                />\n              </PromptInputBody>\n              <PromptInputFooter className=\"flex items-center justify-between gap-3 border-foreground/10 border-t px-3 py-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">ToolLoopAgent</Badge>\n                  <Badge variant=\"outline\">{sandboxProvider}</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  {isBusy ? (\n                    <Button\n                      onClick={stop}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <StopIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  {hasMessages ? (\n                    <Button\n                      onClick={() => regenerate()}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <ArrowClockwiseIcon className=\"size-3.5\" />\n                      Retry\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit\n                    disabled={!isChatAvailable}\n                    status={status}\n                  />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {samplePrompts.map((prompt) => (\n                  <Button\n                    key={prompt}\n                    onClick={() => sendMessage({ text: prompt })}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <WrenchIcon className=\"size-3.5\" />\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </section>\n\n      <aside className=\"border border-foreground/10 bg-background p-4 lg:min-h-0 lg:overflow-y-auto\">\n        <div className=\"space-y-5\">\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Runtime\n            </p>\n            <Badge className=\"mt-2\" variant=\"secondary\">\n              {environmentLabel}\n            </Badge>\n            <p className=\"mt-1 text-muted-foreground text-sm\">\n              {sandboxProvider}\n            </p>\n          </div>\n\n          <div className=\"space-y-2\">\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Available skills\n            </p>\n            {availableSkills.map((skill) => (\n              <div\n                className=\"space-y-1 border border-foreground/10 p-3\"\n                key={skill.name}\n              >\n                <p className=\"font-medium text-sm\">{skill.name}</p>\n                <p className=\"text-muted-foreground text-sm/relaxed\">\n                  {skill.description}\n                </p>\n              </div>\n            ))}\n          </div>\n\n          <div className=\"space-y-2\">\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Configured tools\n            </p>\n            {configuredTools.map((tool) => (\n              <div\n                className=\"space-y-1 border border-foreground/10 p-3\"\n                key={tool.name}\n              >\n                <p className=\"font-medium text-sm\">{tool.name}</p>\n                <p className=\"text-muted-foreground text-sm/relaxed\">\n                  {tool.description}\n                </p>\n              </div>\n            ))}\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Artifact targets\n            </p>\n            <div className=\"mt-2 flex flex-wrap gap-2\">\n              <Badge variant=\"secondary\">artifacts/CONTEXT.md</Badge>\n              <Badge variant=\"secondary\">\n                artifacts/&lt;skill&gt;/SKILL.md\n              </Badge>\n            </div>\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Source core\n            </p>\n            <p className=\"mt-1 text-sm\">\n              The model starts from skill metadata, calls <code>skill</code> for\n              full instructions, then uses sandbox tools for file and shell\n              work.\n            </p>\n          </div>\n        </div>\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/skills-agent/skills-agent-workspace.tsx"
    },
    {
      "path": "registry/skills-agent/components/skills-agent/use-skills-agent-chat.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport } from \"ai\";\nimport { useState } from \"react\";\n\nexport function useSkillsAgentChat() {\n  const [chat] = useState(\n    () =>\n      new Chat({\n        transport: new DefaultChatTransport({\n          api: \"/api/demos/skills-agent\",\n        }),\n      })\n  );\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy =\n    controller.status === \"submitted\" || controller.status === \"streaming\";\n\n  return {\n    ...controller,\n    chat,\n    hasMessages,\n    isBusy,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/skills-agent/use-skills-agent-chat.ts"
    },
    {
      "path": "registry/skills-agent/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/chat.ts",
      "content": "import {\n  createAgentUIStreamResponse,\n  stepCountIs,\n  ToolLoopAgent,\n  type UIMessage,\n} from \"ai\";\nimport { z } from \"zod\";\nimport { createSkillsAgentGateway, getSkillsAgentEnv } from \"./env\";\nimport {\n  resolveSkillsAgentChatModel,\n  SKILLS_AGENT_PROVIDER_OPTIONS,\n} from \"./model\";\nimport type { SkillMetadata } from \"./skill-catalog\";\nimport { createSkillsAgentWorkspace } from \"./workspace\";\n\ntype DemoEnv = Record<string, string | undefined>;\n\nconst skillOptionSchema = z.object({\n  description: z.string(),\n  name: z.string(),\n  path: z.string(),\n});\n\nconst skillsAgentCallOptionsSchema = z.object({\n  skills: z.array(skillOptionSchema).min(1),\n});\n\ntype SkillsAgentCallOptions = z.infer<typeof skillsAgentCallOptionsSchema>;\n\nexport const skillsAgentInstructions = [\n  \"You are the skills-agent demo for a product and engineering team.\",\n  \"Treat the visible skill catalog as lightweight metadata. Load the full SKILL.md only when you decide a skill is needed.\",\n  \"Choose a skill based on the visible catalog descriptions when the user's request matches one of them.\",\n  \"Use readFile, writeFile, and bash inside the active sandbox workspace when the skill instructions require repository context or generated artifacts.\",\n  \"Follow the loaded skill's filesystem conventions. grill-with-docs owns repository paths like CONTEXT.md and docs/adr/, while artifacts/ is for standalone drafts when a skill does not specify a canonical location.\",\n  \"Repository files such as CONTEXT.md or docs/adr/ may not exist yet. Use bash checks like test -f or ls before reading optional files, and create them lazily when the loaded skill calls for that.\",\n  \"Keep the final answer concise and report any artifact paths that were created or updated.\",\n].join(\" \");\n\nexport async function streamSkillsAgent(\n  messages: UIMessage[],\n  {\n    env = getSkillsAgentEnv(),\n    sessionId,\n    skills,\n  }: {\n    env?: DemoEnv;\n    sessionId: string;\n    skills: SkillMetadata[];\n  }\n) {\n  const gateway = createSkillsAgentGateway(env);\n  const chatModel = resolveSkillsAgentChatModel(env);\n  const workspace = await createSkillsAgentWorkspace({\n    env,\n    sessionId,\n    skills,\n  });\n\n  const agent = new ToolLoopAgent<SkillsAgentCallOptions>({\n    instructions: skillsAgentInstructions,\n    model: gateway(chatModel),\n    prepareCall: ({ options, ...call }) => ({\n      ...call,\n      instructions: [\n        skillsAgentInstructions,\n        `Visible skill catalog:\\n${workspace.visibleSkillCatalogText}`,\n        `Sandbox project root: ${workspace.projectRoot}`,\n        `Default artifacts root: ${workspace.artifactsRoot}`,\n      ].join(\"\\n\\n\"),\n    }),\n    providerOptions: SKILLS_AGENT_PROVIDER_OPTIONS,\n    callOptionsSchema: skillsAgentCallOptionsSchema,\n    stopWhen: stepCountIs(20),\n    tools: workspace.toolset.tools,\n  });\n\n  return await createAgentUIStreamResponse({\n    agent,\n    options: {\n      skills: workspace.visibleSkillCatalog,\n    },\n    sendReasoning: true,\n    uiMessages: messages,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/chat.ts"
    },
    {
      "path": "registry/skills-agent/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/env-source.ts",
      "content": "export function getSkillsAgentAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/env-source.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/env.ts",
      "content": "import { getSkillsAgentAppEnv } from \"./env-source\";\nimport {\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n} from \"@/lib/ai-gateway/contract\";\n\nexport const DEFAULT_SKILLS_AGENT_CHAT_MODEL = \"openai/gpt-5-mini\";\nexport const SKILLS_AGENT_SANDBOX_ENVIRONMENT_LABEL =\n  \"Node 24 + uv Python 3.13\";\n\nexport type SkillsAgentEnv = AiGatewayEnvRecord;\n\nexport type SkillsAgentConfig = AiGatewayContractConfig;\n\nexport interface SkillsAgentSandboxTokenCredentials {\n  projectId: string;\n  teamId: string;\n  token: string;\n}\n\nexport interface SkillsAgentSandboxSetupState {\n  authMode: \"local\" | \"oidc\" | \"token\";\n  isReady: boolean;\n  issues: string[];\n  providerLabel: \"Local sandbox\" | \"Vercel Sandbox\";\n  runtime: \"node24\";\n}\n\nexport interface SkillsAgentSetupState\n  extends AiGatewayContractSetupState<AiGatewaySetupConfig> {\n  sandboxProvider: SkillsAgentSandboxSetupState[\"providerLabel\"];\n}\n\nexport type SkillsAgentGateway = ReturnType<typeof createAiGatewayFromContract>;\n\nconst skillsAgentContract = {\n  defaultChatModel: DEFAULT_SKILLS_AGENT_CHAT_MODEL,\n  missingApiKeyError:\n    \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the skills agent.\",\n  missingApiKeyIssue: \"AI_GATEWAY_API_KEY is missing. The demo can render, but skills-agent chat requests will fail until it is configured.\",\n} as const;\n\nexport function getSkillsAgentEnv(): SkillsAgentEnv {\n  return getSkillsAgentAppEnv();\n}\n\nexport function getSkillsAgentConfig(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n): SkillsAgentConfig {\n  return readAiGatewayContractConfig(env, skillsAgentContract);\n}\n\nexport function hasSkillsAgentSandboxTokenCredentials(env: SkillsAgentEnv) {\n  return Boolean(\n    env.VERCEL_PROJECT_ID && env.VERCEL_TEAM_ID && env.VERCEL_TOKEN\n  );\n}\n\nexport function getSkillsAgentSandboxTokenCredentials(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n): SkillsAgentSandboxTokenCredentials {\n  const { VERCEL_PROJECT_ID, VERCEL_TEAM_ID, VERCEL_TOKEN } = env;\n\n  if (!(VERCEL_PROJECT_ID && VERCEL_TEAM_ID && VERCEL_TOKEN)) {\n    throw new Error(\n      \"Vercel Sandbox token credentials are incomplete. Expected VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID.\"\n    );\n  }\n\n  return {\n    projectId: VERCEL_PROJECT_ID,\n    teamId: VERCEL_TEAM_ID,\n    token: VERCEL_TOKEN,\n  };\n}\n\nexport function getSkillsAgentSandboxSetupState(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n): SkillsAgentSandboxSetupState {\n  const issues: string[] = [];\n  const hasOidc = Boolean(env.VERCEL_OIDC_TOKEN);\n  const hasAnyTokenCredential = Boolean(\n    env.VERCEL_PROJECT_ID || env.VERCEL_TEAM_ID || env.VERCEL_TOKEN\n  );\n  let authMode: SkillsAgentSandboxSetupState[\"authMode\"] = \"local\";\n\n  if (hasOidc) {\n    authMode = \"oidc\";\n  } else if (hasSkillsAgentSandboxTokenCredentials(env)) {\n    authMode = \"token\";\n  }\n\n  if (\n    authMode === \"local\" &&\n    hasAnyTokenCredential &&\n    !hasSkillsAgentSandboxTokenCredentials(env)\n  ) {\n    issues.push(\n      \"Vercel Sandbox token credentials are incomplete. Set VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID together, or remove them to use local sandbox mode.\"\n    );\n  }\n\n  return {\n    authMode,\n    isReady: issues.length === 0,\n    issues,\n    providerLabel: authMode === \"local\" ? \"Local sandbox\" : \"Vercel Sandbox\",\n    runtime: \"node24\",\n  };\n}\n\nexport function getSkillsAgentSetupState(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n): SkillsAgentSetupState {\n  const sandboxSetup = getSkillsAgentSandboxSetupState(env);\n  const gatewaySetup = buildAiGatewayContractSetupState(env, {\n    ...skillsAgentContract,\n    getAdditionalIssues: () => sandboxSetup.issues,\n  });\n\n  return {\n    ...gatewaySetup,\n    sandboxProvider: sandboxSetup.providerLabel,\n  };\n}\n\nexport function createSkillsAgentGateway(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n): SkillsAgentGateway {\n  return createAiGatewayFromContract(env, skillsAgentContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/env.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/local-skill-catalog.ts",
      "content": "import { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n  discoverSkills,\n  type SkillMetadata,\n  type SkillsSandbox,\n} from \"./skill-catalog\";\n\nexport const PRIMARY_SKILL_NAMES = [\n  \"grill-with-docs\",\n  \"skill-creator\",\n] as const;\n\nexport const SKILLS_AGENT_WORKSPACE_ROOT = process.cwd();\nconst DEFAULT_SKILLS_DIRECTORY = path.join(\n  SKILLS_AGENT_WORKSPACE_ROOT,\n  \".agents/skills\"\n);\n\nclass LocalSkillCatalogSource implements SkillsSandbox {\n  exec() {\n    return Promise.resolve({\n      stderr: \"\",\n      stdout: \"\",\n    });\n  }\n\n  readdir(directory: string, opts?: { withFileTypes?: boolean }) {\n    if (opts?.withFileTypes) {\n      return readdir(directory, { withFileTypes: true });\n    }\n\n    return readdir(directory);\n  }\n\n  readFile(filePath: string, encoding: BufferEncoding | \"utf-8\") {\n    return readFile(filePath, encoding);\n  }\n}\n\nexport async function discoverWorkspaceSkills(\n  directories: string[] = [DEFAULT_SKILLS_DIRECTORY]\n): Promise<SkillMetadata[]> {\n  const skills = await discoverSkills(\n    new LocalSkillCatalogSource(),\n    directories\n  );\n  const allowedNames = new Set(PRIMARY_SKILL_NAMES);\n\n  return skills.filter((skill) => allowedNames.has(skill.name as never));\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/local-skill-catalog.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/local-sandbox.ts",
      "content": "import { spawn } from \"node:child_process\";\nimport {\n  mkdir,\n  readdir,\n  readFile,\n  rm,\n  stat,\n  writeFile,\n} from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\n\nimport type { SkillsAgentSessionRegistry } from \"./sandbox\";\nimport {\n  discoverSkills,\n  type LoadedSkill,\n  loadSkill,\n  type SkillMetadata,\n  type SkillsSandbox,\n} from \"./skill-catalog\";\n\nconst SANDBOX_PROJECT_ROOT = \"/vercel/sandbox/project\";\nconst SANDBOX_SKILLS_ROOT = `${SANDBOX_PROJECT_ROOT}/.agents/skills`;\nconst localSandboxRoot = path.join(\n  /*turbopackIgnore: true*/ tmpdir(),\n  \"ai-sdk-skills-agent\"\n);\nconst commandTimeoutMs = 30_000;\nconst maxOutputBytes = 64 * 1024;\n\ninterface SessionEntry {\n  activeSkillDirectory: string | null;\n  hydratedSkillNames: Set<string>;\n}\n\nfunction sanitizeSessionId(sessionId: string) {\n  return sessionId.replace(/[^a-zA-Z0-9_-]/g, \"_\").slice(0, 96);\n}\n\nfunction getSessionRoot(sessionId: string) {\n  return path.join(\n    /*turbopackIgnore: true*/ localSandboxRoot,\n    sanitizeSessionId(sessionId)\n  );\n}\n\nfunction isInsideDirectory(parent: string, child: string) {\n  const relativePath = path.relative(parent, child);\n\n  return (\n    relativePath === \"\" ||\n    (!relativePath.startsWith(\"..\") && !path.isAbsolute(relativePath))\n  );\n}\n\nfunction toProjectRelativePath(projectRoot: string, targetPath: string) {\n  if (targetPath.startsWith(SANDBOX_PROJECT_ROOT)) {\n    return path.relative(SANDBOX_PROJECT_ROOT, targetPath) || \".\";\n  }\n\n  if (path.isAbsolute(targetPath)) {\n    if (!isInsideDirectory(projectRoot, targetPath)) {\n      throw new Error(`Sandbox path escapes the local workspace: ${targetPath}`);\n    }\n\n    return path.relative(projectRoot, targetPath) || \".\";\n  }\n\n  return targetPath;\n}\n\nfunction resolveLocalPath(projectRoot: string, targetPath: string) {\n  const resolvedPath = path.resolve(\n    /*turbopackIgnore: true*/ projectRoot,\n    toProjectRelativePath(projectRoot, targetPath)\n  );\n\n  if (!isInsideDirectory(projectRoot, resolvedPath)) {\n    throw new Error(`Sandbox path escapes the local workspace: ${targetPath}`);\n  }\n\n  return resolvedPath;\n}\n\nfunction resolveSessionPath(\n  entry: SessionEntry,\n  projectRoot: string,\n  targetPath: string\n) {\n  if (\n    entry.activeSkillDirectory &&\n    (targetPath === \".\" ||\n      targetPath.startsWith(\"./\") ||\n      targetPath.startsWith(\"../\"))\n  ) {\n    return path.resolve(\n      /*turbopackIgnore: true*/ entry.activeSkillDirectory,\n      targetPath\n    );\n  }\n\n  return resolveLocalPath(projectRoot, targetPath);\n}\n\nasync function pathExists(localPath: string) {\n  try {\n    await stat(/*turbopackIgnore: true*/ localPath);\n    return true;\n  } catch (error) {\n    if (\n      typeof error === \"object\" &&\n      error !== null &&\n      \"code\" in error &&\n      error.code === \"ENOENT\"\n    ) {\n      return false;\n    }\n\n    throw error;\n  }\n}\n\nfunction appendBounded(current: string, chunk: Buffer) {\n  const next = current + chunk.toString(\"utf-8\");\n\n  return next.length > maxOutputBytes ? next.slice(-maxOutputBytes) : next;\n}\n\nfunction runLocalCommand(command: string, cwd: string) {\n  return new Promise<{\n    command: string;\n    exitCode: number;\n    stderr: string;\n    stdout: string;\n  }>((resolve, reject) => {\n    let stdout = \"\";\n    let stderr = \"\";\n    let timedOut = false;\n    const child = spawn(\"bash\", [\"-lc\", command], {\n      cwd,\n      env: process.env,\n    });\n    const timeout = setTimeout(() => {\n      timedOut = true;\n      child.kill(\"SIGTERM\");\n    }, commandTimeoutMs);\n\n    child.stdout.on(\"data\", (chunk: Buffer) => {\n      stdout = appendBounded(stdout, chunk);\n    });\n    child.stderr.on(\"data\", (chunk: Buffer) => {\n      stderr = appendBounded(stderr, chunk);\n    });\n    child.on(\"error\", (error) => {\n      clearTimeout(timeout);\n      reject(error);\n    });\n    child.on(\"close\", (code) => {\n      clearTimeout(timeout);\n      resolve({\n        command,\n        exitCode: timedOut ? 124 : (code ?? 1),\n        stderr: timedOut\n          ? [stderr, `Command timed out after ${commandTimeoutMs}ms.`]\n              .filter(Boolean)\n              .join(\"\\n\")\n          : stderr,\n        stdout,\n      });\n    });\n  });\n}\n\nclass LocalSkillCatalogSource implements SkillsSandbox {\n  readdir(directory: string, opts?: { withFileTypes?: boolean }) {\n    if (opts?.withFileTypes) {\n      return readdir(/*turbopackIgnore: true*/ directory, {\n        withFileTypes: true,\n      });\n    }\n\n    return readdir(/*turbopackIgnore: true*/ directory);\n  }\n\n  readFile(filePath: string, encoding: BufferEncoding | \"utf-8\") {\n    return readFile(/*turbopackIgnore: true*/ filePath, encoding);\n  }\n\n  exec() {\n    return Promise.resolve({\n      stderr: \"\",\n      stdout: \"\",\n    });\n  }\n}\n\nasync function copyLocalPathToLocalSandbox(\n  localPath: string,\n  sandboxPath: string\n) {\n  const entry = await stat(/*turbopackIgnore: true*/ localPath);\n\n  if (entry.isDirectory()) {\n    await writeDirectory(sandboxPath);\n\n    for (const child of await readdir(/*turbopackIgnore: true*/ localPath)) {\n      await copyLocalPathToLocalSandbox(\n        path.join(/*turbopackIgnore: true*/ localPath, child),\n        path.join(/*turbopackIgnore: true*/ sandboxPath, child)\n      );\n    }\n\n    return;\n  }\n\n  await writeDirectory(path.dirname(sandboxPath));\n  await writeFile(\n    /*turbopackIgnore: true*/ sandboxPath,\n    await readFile(/*turbopackIgnore: true*/ localPath)\n  );\n}\n\nasync function writeDirectory(directory: string) {\n  await mkdir(/*turbopackIgnore: true*/ directory, { recursive: true });\n}\n\nfunction mergeSkillCatalog(\n  primarySkills: SkillMetadata[],\n  fallbackSkills: SkillMetadata[]\n) {\n  const seen = new Set<string>();\n  const mergedSkills: SkillMetadata[] = [];\n\n  for (const skill of [...primarySkills, ...fallbackSkills]) {\n    const key = skill.name.toLowerCase();\n\n    if (seen.has(key)) {\n      continue;\n    }\n\n    seen.add(key);\n    mergedSkills.push(skill);\n  }\n\n  return mergedSkills;\n}\n\nfunction findSkill(skills: SkillMetadata[], name: string) {\n  return skills.find(\n    (candidate) => candidate.name.toLowerCase() === name.toLowerCase()\n  );\n}\n\nfunction toLocalSandboxSkillPath(projectRoot: string, skill: SkillMetadata) {\n  return path.join(\n    /*turbopackIgnore: true*/ projectRoot,\n    \".agents/skills\",\n    path.basename(skill.path)\n  );\n}\n\nfunction isSandboxSkillPath(projectRoot: string, skillPath: string) {\n  return (\n    skillPath.startsWith(SANDBOX_SKILLS_ROOT) ||\n    isInsideDirectory(\n      path.join(/*turbopackIgnore: true*/ projectRoot, \".agents/skills\"),\n      skillPath\n    )\n  );\n}\n\nexport function createLocalSkillsAgentSessionRegistry(): SkillsAgentSessionRegistry {\n  const entries = new Map<string, SessionEntry>();\n\n  const getOrCreateEntry = (sessionId: string) => {\n    const existingEntry = entries.get(sessionId);\n\n    if (existingEntry) {\n      return existingEntry;\n    }\n\n    const createdEntry: SessionEntry = {\n      activeSkillDirectory: null,\n      hydratedSkillNames: new Set<string>(),\n    };\n    entries.set(sessionId, createdEntry);\n    return createdEntry;\n  };\n\n  const discoverLocalSkills = async (projectRoot: string) => {\n    const skillsDirectory = path.join(\n      /*turbopackIgnore: true*/ projectRoot,\n      \".agents/skills\"\n    );\n\n    if (!(await pathExists(skillsDirectory))) {\n      return [];\n    }\n\n    return discoverSkills(new LocalSkillCatalogSource(), [skillsDirectory]);\n  };\n\n  const ensureSkillHydrated = async (\n    entry: SessionEntry,\n    projectRoot: string,\n    skill: SkillMetadata\n  ) => {\n    if (entry.hydratedSkillNames.has(skill.name)) {\n      return;\n    }\n\n    const sandboxPath = toLocalSandboxSkillPath(projectRoot, skill);\n\n    if (!isSandboxSkillPath(projectRoot, skill.path)) {\n      await copyLocalPathToLocalSandbox(skill.path, sandboxPath);\n    }\n\n    entry.hydratedSkillNames.add(skill.name);\n  };\n\n  return {\n    getSession(sessionId) {\n      const entry = getOrCreateEntry(sessionId);\n      const sessionRoot = getSessionRoot(sessionId);\n      const projectRoot = path.join(\n        /*turbopackIgnore: true*/ sessionRoot,\n        \"project\"\n      );\n      const artifactsRoot = path.join(\n        /*turbopackIgnore: true*/ projectRoot,\n        \"artifacts\"\n      );\n\n      return {\n        get activeSkillDirectory() {\n          return entry.activeSkillDirectory;\n        },\n        artifactsRoot,\n        async discoverSkills(skills) {\n          return mergeSkillCatalog(await discoverLocalSkills(projectRoot), skills);\n        },\n        async listSkillFiles(skillDirectory) {\n          const resolvedDirectory = resolveLocalPath(projectRoot, skillDirectory);\n\n          if (!(await pathExists(resolvedDirectory))) {\n            return [];\n          }\n\n          const files: string[] = [];\n          const walk = async (directory: string) => {\n            for (const child of await readdir(\n              /*turbopackIgnore: true*/ directory,\n              {\n                withFileTypes: true,\n              }\n            )) {\n              const childPath = path.join(\n                /*turbopackIgnore: true*/ directory,\n                child.name\n              );\n\n              if (child.isDirectory()) {\n                await walk(childPath);\n                continue;\n              }\n\n              if (child.isFile() && child.name !== \"SKILL.md\") {\n                files.push(path.relative(resolvedDirectory, childPath));\n              }\n            }\n          };\n\n          await walk(resolvedDirectory);\n          return files.sort((left, right) => left.localeCompare(right));\n        },\n        async loadSkill(skills, name): Promise<LoadedSkill> {\n          const availableSkills = mergeSkillCatalog(\n            await discoverLocalSkills(projectRoot),\n            skills\n          );\n          const skill = findSkill(availableSkills, name);\n\n          if (!skill) {\n            throw new Error(`Skill \"${name}\" was not found in the catalog.`);\n          }\n\n          await ensureSkillHydrated(entry, projectRoot, skill);\n\n          const localSkillPath = toLocalSandboxSkillPath(projectRoot, skill);\n          const loadedSkill = await loadSkill(\n            new LocalSkillCatalogSource(),\n            [\n              {\n                ...skill,\n                path: localSkillPath,\n              },\n            ],\n            name\n          );\n\n          entry.activeSkillDirectory = loadedSkill.skillDirectory;\n          return loadedSkill;\n        },\n        async pathExists(targetPath) {\n          return pathExists(resolveSessionPath(entry, projectRoot, targetPath));\n        },\n        projectRoot,\n        async readFile(targetPath) {\n          return readFile(\n            /*turbopackIgnore: true*/ resolveSessionPath(\n              entry,\n              projectRoot,\n              targetPath\n            ),\n            \"utf-8\"\n          );\n        },\n        async runCommand(command) {\n          await writeDirectory(projectRoot);\n          await writeDirectory(artifactsRoot);\n\n          return runLocalCommand(command, projectRoot);\n        },\n        sessionId,\n        async stop() {\n          await rm(/*turbopackIgnore: true*/ sessionRoot, {\n            force: true,\n            recursive: true,\n          });\n        },\n        async writeFile(targetPath, content) {\n          const resolvedPath = resolveSessionPath(\n            entry,\n            projectRoot,\n            targetPath\n          );\n\n          await writeDirectory(path.dirname(resolvedPath));\n          await writeFile(\n            /*turbopackIgnore: true*/ resolvedPath,\n            content,\n            \"utf-8\"\n          );\n\n          return {\n            bytes: Buffer.byteLength(content, \"utf-8\"),\n            path: resolvedPath,\n          };\n        },\n      };\n    },\n    async stopSession(sessionId) {\n      await rm(/*turbopackIgnore: true*/ getSessionRoot(sessionId), {\n        force: true,\n        recursive: true,\n      });\n      entries.delete(sessionId);\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/local-sandbox.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/model.ts",
      "content": "import { getSkillsAgentEnv } from \"./env\";\n\nconst DEFAULT_SKILLS_AGENT_CHAT_MODEL = \"openai/gpt-5-mini\";\n\nexport const SKILLS_AGENT_PROVIDER_OPTIONS = {\n  openai: {\n    reasoningEffort: \"medium\",\n    reasoningSummary: \"auto\",\n  },\n} as const;\n\ntype SkillsAgentEnv = Record<string, string | undefined>;\n\nexport function resolveSkillsAgentChatModel(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n): string {\n  return env.AI_GATEWAY_CHAT_MODEL || DEFAULT_SKILLS_AGENT_CHAT_MODEL;\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/model.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/official-tools.ts",
      "content": "import { type ToolSet, tool } from \"ai\";\nimport { z } from \"zod\";\n\nimport type { SkillsAgentSession } from \"./sandbox\";\nimport type { SkillMetadata } from \"./skill-catalog\";\n\nconst SKILL_NAME_ALIAS_SEPARATOR_PATTERN = /[\\s_]+/g;\nconst REPEATED_DASH_PATTERN = /-+/g;\n\nconst SKILLS_AGENT_TOOL_PROMPT = [\n  \"Use bash to explore the sandbox workspace and inspect files after loading a skill.\",\n  \"Use readFile and writeFile for direct file access.\",\n  \"Load a skill before using sandbox tools that depend on repository context.\",\n].join(\" \");\n\nexport interface SkillsAgentOfficialTools {\n  availableSkills: Pick<SkillMetadata, \"description\" | \"name\">[];\n  primedFiles: { content: string; path: string }[];\n  tools: ToolSet;\n}\n\ninterface SkillToolSuccessResult {\n  files: string[];\n  instructions: string;\n  skill: {\n    description: string;\n    name: string;\n    path: string;\n  };\n  success: true;\n}\n\nfunction normalizeSkillLookupKey(skillName: string) {\n  return skillName\n    .trim()\n    .toLowerCase()\n    .replace(SKILL_NAME_ALIAS_SEPARATOR_PATTERN, \"-\")\n    .replace(REPEATED_DASH_PATTERN, \"-\");\n}\n\nfunction resolveSkillNameAlias(\n  availableSkills: SkillMetadata[],\n  requestedSkillName: string\n) {\n  const trimmedSkillName = requestedSkillName.trim();\n  const exactMatch = availableSkills.find(\n    (skill) => skill.name === trimmedSkillName\n  );\n\n  if (exactMatch) {\n    return exactMatch.name;\n  }\n\n  const normalizedRequestedSkillName =\n    normalizeSkillLookupKey(trimmedSkillName);\n  const normalizedMatch = availableSkills.find(\n    (skill) =>\n      normalizeSkillLookupKey(skill.name) === normalizedRequestedSkillName\n  );\n\n  return normalizedMatch?.name ?? trimmedSkillName;\n}\n\nfunction findSkillByName(skills: SkillMetadata[], name: string) {\n  return skills.find(\n    (skill) => skill.name.toLowerCase() === name.toLowerCase()\n  );\n}\n\nfunction createSkillToolError(error: unknown) {\n  return {\n    error: error instanceof Error ? error.message : String(error),\n    success: false,\n  };\n}\n\nasync function createLoadedSkillToolResult({\n  availableSkills,\n  loadedSkill,\n  session,\n}: {\n  availableSkills: SkillMetadata[];\n  loadedSkill: Awaited<ReturnType<SkillsAgentSession[\"loadSkill\"]>>;\n  session: SkillsAgentSession;\n}): Promise<SkillToolSuccessResult> {\n  const metadata = findSkillByName(availableSkills, loadedSkill.name);\n\n  if (!metadata) {\n    throw new Error(`Loaded skill \"${loadedSkill.name}\" is missing metadata.`);\n  }\n\n  return {\n    files: await session.listSkillFiles(loadedSkill.skillDirectory),\n    instructions: loadedSkill.content,\n    skill: {\n      description: metadata.description,\n      name: loadedSkill.name,\n      path: loadedSkill.skillDirectory,\n    },\n    success: true,\n  };\n}\n\nfunction buildPrimedFiles({\n  agentsContent,\n  projectRoot,\n}: {\n  agentsContent: string;\n  projectRoot: string;\n}) {\n  return [\n    {\n      content: agentsContent,\n      path: `${projectRoot}/AGENTS.md`,\n    },\n  ];\n}\n\nexport async function createSkillsAgentOfficialTools({\n  agentsContent,\n  projectRoot,\n  session,\n  skills,\n}: {\n  agentsContent: string;\n  projectRoot: string;\n  session: SkillsAgentSession;\n  skills: SkillMetadata[];\n}): Promise<SkillsAgentOfficialTools> {\n  return {\n    availableSkills: skills.map(({ description, name }) => ({\n      description,\n      name,\n    })),\n    primedFiles: buildPrimedFiles({\n      agentsContent,\n      projectRoot,\n    }),\n    tools: {\n      bash: tool({\n        description: [\n          `Run a shell command inside the sandbox workspace at ${projectRoot}.`,\n          SKILLS_AGENT_TOOL_PROMPT,\n        ].join(\"\\n\\n\"),\n        execute: ({ command }) => session.runCommand(command),\n        inputSchema: z.object({\n          command: z\n            .string()\n            .min(1)\n            .describe(\"Shell command to run from the sandbox project root.\"),\n        }),\n      }),\n      readFile: tool({\n        description: \"Read a UTF-8 file from the sandbox workspace.\",\n        execute: async ({ path }) => ({\n          content: await session.readFile(path),\n          path,\n        }),\n        inputSchema: z.object({\n          path: z\n            .string()\n            .min(1)\n            .describe(\n              \"File path to read, relative to the sandbox project root unless absolute.\"\n            ),\n        }),\n      }),\n      skill: tool({\n        description:\n          \"Load full SKILL.md instructions from the visible skills catalog.\",\n        execute: async ({ skillName }) => {\n          try {\n            const availableSkills = await session.discoverSkills(skills);\n            const canonicalSkillName = resolveSkillNameAlias(\n              availableSkills,\n              skillName\n            );\n            const loadedSkill = await session.loadSkill(\n              availableSkills,\n              canonicalSkillName\n            );\n\n            return createLoadedSkillToolResult({\n              availableSkills,\n              loadedSkill,\n              session,\n            });\n          } catch (error) {\n            return createSkillToolError(error);\n          }\n        },\n        inputSchema: z.object({\n          skillName: z\n            .string()\n            .min(1)\n            .describe(\"Name of the skill to load from the visible catalog.\"),\n        }),\n      }),\n      writeFile: tool({\n        description: \"Write a UTF-8 file into the sandbox workspace.\",\n        execute: ({ content, path }) => session.writeFile(path, content),\n        inputSchema: z.object({\n          content: z.string().describe(\"UTF-8 file content to write.\"),\n          path: z\n            .string()\n            .min(1)\n            .describe(\n              \"File path to write, relative to the sandbox project root unless absolute.\"\n            ),\n        }),\n      }),\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/official-tools.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/runtime.ts",
      "content": "import {\n  getSkillsAgentEnv,\n  getSkillsAgentSetupState,\n  SKILLS_AGENT_SANDBOX_ENVIRONMENT_LABEL,\n} from \"./env\";\nimport { discoverWorkspaceSkills } from \"./local-skill-catalog\";\nimport type { SkillMetadata } from \"./skill-catalog\";\n\ntype DemoEnv = Record<string, string | undefined>;\n\nexport interface SkillsAgentRuntimeState {\n  availableSkills: Pick<SkillMetadata, \"description\" | \"name\">[];\n  chatModel: string;\n  environmentLabel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  sandboxProvider: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nexport async function getSkillsAgentRuntimeState(\n  env: DemoEnv = getSkillsAgentEnv(),\n  dependencies: {\n    discoverSkills: () => Promise<SkillMetadata[]>;\n  } = {\n    discoverSkills: discoverWorkspaceSkills,\n  }\n): Promise<SkillsAgentRuntimeState> {\n  const setup = getSkillsAgentSetupState(env);\n  const skills = await dependencies.discoverSkills();\n  const issues = [...setup.issues];\n\n  if (skills.length === 0) {\n    issues.push(\n      'No primary skills were found under \".agents/skills\". The demo expects grill-with-docs and skill-creator.'\n    );\n  }\n\n  return {\n    availableSkills: skills.map(({ description, name }) => ({\n      description,\n      name,\n    })),\n    chatModel: setup.config.chatModel,\n    environmentLabel: SKILLS_AGENT_SANDBOX_ENVIRONMENT_LABEL,\n    isChatAvailable: issues.length === 0,\n    nodeVersion: setup.nodeVersion,\n    sandboxProvider: setup.sandboxProvider,\n    setupMessage: issues.length > 0 ? issues.join(\" \") : null,\n    statusLabel: issues.length === 0 ? \"Ready\" : \"Setup required\",\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/runtime.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/request.ts",
      "content": "import { type UIMessage, validateUIMessages } from \"ai\";\n\nimport { getSkillsAgentEnv } from \"./env\";\nimport { discoverWorkspaceSkills } from \"./local-skill-catalog\";\nimport { getSkillsAgentRuntimeState } from \"./runtime\";\nimport type { SkillMetadata } from \"./skill-catalog\";\n\ntype DemoEnv = Record<string, string | undefined>;\n\ninterface SkillsAgentRequestBody {\n  id?: string;\n  messages?: UIMessage[];\n}\n\ninterface StreamSkillsAgentOptions {\n  sessionId: string;\n  skills: SkillMetadata[];\n}\n\ninterface SkillsAgentRequestDependencies {\n  discoverSkills: () => Promise<SkillMetadata[]>;\n  streamSkillsAgent: (\n    messages: UIMessage[],\n    options: StreamSkillsAgentOptions\n  ) => Promise<Response> | Response;\n}\n\nconst invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\nconst invalidChatIdError =\n  'Expected a JSON body with an \"id\" string and a \"messages\" array.';\n\nasync function readSkillsAgentRequest(\n  body: unknown\n): Promise<{ messages: UIMessage[]; sessionId: string }> {\n  const { id, messages } = (body ?? {}) as SkillsAgentRequestBody;\n\n  if (!(typeof id === \"string\" && id.length > 0 && Array.isArray(messages))) {\n    throw new Error(invalidChatIdError);\n  }\n\n  try {\n    return {\n      messages: await validateUIMessages({ messages }),\n      sessionId: id,\n    };\n  } catch {\n    throw new Error(invalidUiMessagesError);\n  }\n}\n\nexport async function handleSkillsAgentRequest(\n  request: Request,\n  env: DemoEnv = getSkillsAgentEnv(),\n  dependencies: Partial<SkillsAgentRequestDependencies> = {\n    discoverSkills: discoverWorkspaceSkills,\n    streamSkillsAgent: async (messages, options) => {\n      const { streamSkillsAgent } = await import(\"./chat\");\n\n      return streamSkillsAgent(messages, {\n        env,\n        sessionId: options.sessionId,\n        skills: options.skills,\n      });\n    },\n  }\n) {\n  const discoverSkills = dependencies.discoverSkills ?? discoverWorkspaceSkills;\n  const streamAgent =\n    dependencies.streamSkillsAgent ??\n    (async (messages: UIMessage[], options: StreamSkillsAgentOptions) => {\n      const { streamSkillsAgent } = await import(\"./chat\");\n\n      return streamSkillsAgent(messages, {\n        env,\n        sessionId: options.sessionId,\n        skills: options.skills,\n      });\n    });\n  const runtimeState = await getSkillsAgentRuntimeState(env, {\n    discoverSkills,\n  });\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const { messages, sessionId } = await readSkillsAgentRequest(\n      await request.json()\n    );\n    const skills = await discoverSkills();\n\n    return streamAgent(messages, { sessionId, skills });\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      [invalidUiMessagesError, invalidChatIdError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/request.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/sandbox.ts",
      "content": "import path, { posix as posixPath } from \"node:path\";\nimport type {\n  VercelSandboxFactory,\n  VercelSandboxHandle,\n  VercelSandboxSession,\n  VercelSandboxSessionRegistry,\n} from \"./vercel-sandbox\";\nimport {\n  getSkillsAgentEnv,\n  getSkillsAgentSandboxSetupState as getSkillsAgentSandboxSetup,\n  type SkillsAgentEnv,\n  type SkillsAgentSandboxSetupState,\n} from \"./env\";\nimport { SKILLS_AGENT_WORKSPACE_ROOT } from \"./local-skill-catalog\";\nimport {\n  type LoadedSkill,\n  loadSkill,\n  parseSkillMetadata,\n  type SkillMetadata,\n} from \"./skill-catalog\";\n\nexport type SkillsAgentSandboxHandle = VercelSandboxHandle;\nexport type SandboxFactory = VercelSandboxFactory;\nexport const SANDBOX_PROJECT_ROOT = \"/vercel/sandbox/project\";\nexport const SANDBOX_SKILLS_ROOT = `${SANDBOX_PROJECT_ROOT}/.agents/skills`;\nexport const SANDBOX_ARTIFACTS_ROOT = `${SANDBOX_PROJECT_ROOT}/artifacts`;\nexport const SANDBOX_AGENTS_FILE = `${SANDBOX_PROJECT_ROOT}/AGENTS.md`;\n\nexport interface SkillsAgentSession extends VercelSandboxSession {\n  readonly activeSkillDirectory: string | null;\n  discoverSkills(skills: SkillMetadata[]): Promise<SkillMetadata[]>;\n  listSkillFiles(skillDirectory: string): Promise<string[]>;\n  loadSkill(skills: SkillMetadata[], name: string): Promise<LoadedSkill>;\n}\n\nexport interface SkillsAgentSessionRegistry {\n  getSession(sessionId: string): SkillsAgentSession;\n  stopSession(sessionId: string): Promise<void>;\n}\n\ninterface SessionEntry {\n  activeSkillDirectory: string | null;\n  hydratedSkillNames: Set<string>;\n}\n\nfunction resolveSessionPath(entry: SessionEntry, targetPath: string) {\n  if (targetPath.startsWith(\"/\")) {\n    return targetPath;\n  }\n\n  if (\n    entry.activeSkillDirectory &&\n    (targetPath === \".\" ||\n      targetPath.startsWith(\"./\") ||\n      targetPath.startsWith(\"../\"))\n  ) {\n    return posixPath.normalize(\n      posixPath.join(entry.activeSkillDirectory, targetPath)\n    );\n  }\n\n  return targetPath;\n}\n\nfunction shellQuote(value: string) {\n  return `'${value.replace(/'/g, `'\"'\"'`)}'`;\n}\n\nfunction isSandboxSkillPath(skillPath: string) {\n  return (\n    skillPath === SANDBOX_SKILLS_ROOT ||\n    skillPath.startsWith(`${SANDBOX_SKILLS_ROOT}/`)\n  );\n}\n\nfunction createSandboxSkillSource(baseSession: VercelSandboxSession) {\n  return {\n    exec: async () => ({\n      stderr: \"\",\n      stdout: \"\",\n    }),\n    readdir: async () => [],\n    readFile: (filePath: string, encoding: BufferEncoding | \"utf-8\") =>\n      baseSession.readFile(filePath).then((content) => {\n        if (encoding !== \"utf-8\") {\n          throw new Error(`Unexpected encoding ${encoding}`);\n        }\n\n        return content;\n      }),\n  };\n}\n\nfunction mergeSkillCatalog(\n  primarySkills: SkillMetadata[],\n  fallbackSkills: SkillMetadata[]\n) {\n  const seen = new Set<string>();\n  const mergedSkills: SkillMetadata[] = [];\n\n  for (const skill of [...primarySkills, ...fallbackSkills]) {\n    const key = skill.name.toLowerCase();\n\n    if (seen.has(key)) {\n      continue;\n    }\n\n    seen.add(key);\n    mergedSkills.push(skill);\n  }\n\n  return mergedSkills;\n}\n\nfunction findSkill(skills: SkillMetadata[], name: string) {\n  return skills.find(\n    (candidate) => candidate.name.toLowerCase() === name.toLowerCase()\n  );\n}\n\nasync function discoverSandboxSkills(baseSession: VercelSandboxSession) {\n  const command = [\n    `if [ -d ${shellQuote(SANDBOX_SKILLS_ROOT)} ]; then`,\n    `find ${shellQuote(SANDBOX_SKILLS_ROOT)} -mindepth 2 -maxdepth 2 -type f -name 'SKILL.md' -print`,\n    \"fi\",\n  ].join(\"\\n\");\n  const result = await baseSession.runCommand(command);\n\n  if (result.exitCode !== 0) {\n    throw new Error(\n      `Sandbox skill discovery failed. ${result.stderr.trim() || result.stdout.trim() || result.command}`\n    );\n  }\n\n  const seen = new Set<string>();\n  const skills: SkillMetadata[] = [];\n  const skillFiles = result.stdout\n    .split(/\\r?\\n/)\n    .map((line) => line.trim())\n    .filter(Boolean)\n    .sort((left, right) => left.localeCompare(right));\n\n  for (const skillFile of skillFiles) {\n    const content = await baseSession.readFile(skillFile);\n    const metadata = parseSkillMetadata(content);\n\n    if (!metadata) {\n      continue;\n    }\n\n    const key = metadata.name.toLowerCase();\n\n    if (seen.has(key)) {\n      continue;\n    }\n\n    seen.add(key);\n    skills.push({\n      ...metadata,\n      path: posixPath.dirname(skillFile),\n    });\n  }\n\n  return skills;\n}\n\nasync function listSandboxSkillFiles(\n  baseSession: VercelSandboxSession,\n  skillDirectory: string\n) {\n  const command = [\n    `if [ -d ${shellQuote(skillDirectory)} ]; then`,\n    `cd ${shellQuote(skillDirectory)} && find . -type f ! -path './SKILL.md' -print | sed 's#^./##' | sort`,\n    \"fi\",\n  ].join(\"\\n\");\n  const result = await baseSession.runCommand(command);\n\n  if (result.exitCode !== 0) {\n    throw new Error(\n      `Sandbox skill file listing failed for ${skillDirectory}. ${result.stderr.trim() || result.stdout.trim() || result.command}`\n    );\n  }\n\n  return result.stdout\n    .split(/\\r?\\n/)\n    .map((line) => line.trim())\n    .filter(Boolean);\n}\n\nexport const getSkillsAgentSandboxSetupState = getSkillsAgentSandboxSetup;\n\nexport async function createSkillsAgentSandbox(\n  sessionId: string,\n  env: SkillsAgentEnv = getSkillsAgentEnv(),\n  options: { ports?: number[] } = {}\n): Promise<SkillsAgentSandboxHandle> {\n  const { createVercelSandbox } = await import(\"./vercel-sandbox\");\n\n  return createVercelSandbox(sessionId, env, options);\n}\n\nexport async function createSkillsAgentSessionRegistry({\n  createSandbox,\n  workspaceRoot = SKILLS_AGENT_WORKSPACE_ROOT,\n}: {\n  createSandbox: SandboxFactory;\n  workspaceRoot?: string;\n}): Promise<SkillsAgentSessionRegistry> {\n  const { copyLocalPathToSandbox, createVercelSandboxSessionRegistry } =\n    await import(\"./vercel-sandbox\");\n  const baseRegistry: VercelSandboxSessionRegistry =\n    createVercelSandboxSessionRegistry({\n      createSandbox,\n      workspaceRoot,\n    });\n  const entries = new Map<string, SessionEntry>();\n\n  const getOrCreateEntry = (sessionId: string) => {\n    const existingEntry = entries.get(sessionId);\n\n    if (existingEntry) {\n      return existingEntry;\n    }\n\n    const createdEntry: SessionEntry = {\n      activeSkillDirectory: null,\n      hydratedSkillNames: new Set<string>(),\n    };\n    entries.set(sessionId, createdEntry);\n    return createdEntry;\n  };\n\n  const ensureSkillHydrated = async (\n    entry: SessionEntry,\n    sessionId: string,\n    skill: SkillMetadata\n  ) => {\n    if (entry.hydratedSkillNames.has(skill.name)) {\n      return;\n    }\n\n    const sandbox = await baseRegistry.getSandbox(sessionId);\n    await copyLocalPathToSandbox(\n      sandbox,\n      skill.path,\n      posixPath.join(SANDBOX_SKILLS_ROOT, path.basename(skill.path))\n    );\n    entry.hydratedSkillNames.add(skill.name);\n  };\n\n  return {\n    getSession(sessionId) {\n      const entry = getOrCreateEntry(sessionId);\n      const baseSession = baseRegistry.getSession(sessionId);\n\n      return {\n        get activeSkillDirectory() {\n          return entry.activeSkillDirectory;\n        },\n        artifactsRoot: SANDBOX_ARTIFACTS_ROOT,\n        async discoverSkills(skills) {\n          return mergeSkillCatalog(\n            await discoverSandboxSkills(baseSession),\n            skills\n          );\n        },\n        listSkillFiles(skillDirectory) {\n          return listSandboxSkillFiles(baseSession, skillDirectory);\n        },\n        projectRoot: SANDBOX_PROJECT_ROOT,\n        sessionId,\n        async loadSkill(skills, name) {\n          const availableSkills = mergeSkillCatalog(\n            await discoverSandboxSkills(baseSession),\n            skills\n          );\n          const skill = findSkill(availableSkills, name);\n\n          if (!skill) {\n            throw new Error(`Skill \"${name}\" was not found in the catalog.`);\n          }\n\n          if (!isSandboxSkillPath(skill.path)) {\n            await ensureSkillHydrated(entry, sessionId, skill);\n          }\n\n          return loadSkill(\n            createSandboxSkillSource(baseSession),\n            [\n              {\n                ...skill,\n                path: isSandboxSkillPath(skill.path)\n                  ? skill.path\n                  : posixPath.join(\n                      SANDBOX_SKILLS_ROOT,\n                      path.basename(skill.path)\n                    ),\n              },\n            ],\n            name\n          ).then((loadedSkill) => {\n            entry.activeSkillDirectory = loadedSkill.skillDirectory;\n            return loadedSkill;\n          });\n        },\n        pathExists(targetPath) {\n          return baseSession.pathExists(resolveSessionPath(entry, targetPath));\n        },\n        readFile(targetPath) {\n          return baseSession.readFile(resolveSessionPath(entry, targetPath));\n        },\n        runCommand(command) {\n          return baseSession.runCommand(command);\n        },\n        stop() {\n          return baseRegistry.stopSession(sessionId);\n        },\n        writeFile(targetPath, content) {\n          return baseSession.writeFile(\n            resolveSessionPath(entry, targetPath),\n            content\n          );\n        },\n      };\n    },\n    async stopSession(sessionId) {\n      await baseRegistry.stopSession(sessionId);\n      entries.delete(sessionId);\n    },\n  };\n}\n\nlet sharedRegistry: SkillsAgentSessionRegistry | null = null;\nlet sharedRegistryMode:\n  | ReturnType<typeof getSkillsAgentSandboxSetup>[\"authMode\"]\n  | null = null;\n\nexport async function getSharedSkillsAgentSessionRegistry(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n) {\n  const setupState = getSkillsAgentSandboxSetup(env);\n\n  if (!sharedRegistry || sharedRegistryMode !== setupState.authMode) {\n    if (setupState.authMode === \"local\") {\n      const { createLocalSkillsAgentSessionRegistry } = await import(\n        \"./local-sandbox\"\n      );\n\n      sharedRegistry = createLocalSkillsAgentSessionRegistry();\n    } else {\n      const { createVercelSandbox } = await import(\"./vercel-sandbox\");\n\n      sharedRegistry = await createSkillsAgentSessionRegistry({\n        createSandbox: (sessionId) => createVercelSandbox(sessionId, env),\n      });\n    }\n\n    sharedRegistryMode = setupState.authMode;\n  }\n\n  return sharedRegistry;\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/sandbox.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/skill-catalog.ts",
      "content": "interface SkillDirent {\n  isDirectory(): boolean;\n  name: string;\n}\n\nexport interface SkillsSandbox {\n  exec(command: string): Promise<{ stderr: string; stdout: string }>;\n  readdir(\n    path: string,\n    opts?: { withFileTypes?: boolean }\n  ): Promise<SkillDirent[] | string[]>;\n  readFile(path: string, encoding: BufferEncoding | \"utf-8\"): Promise<string>;\n}\n\nexport interface SkillMetadata {\n  description: string;\n  name: string;\n  path: string;\n}\n\nexport interface LoadedSkill {\n  content: string;\n  name: string;\n  skillDirectory: string;\n}\n\nconst frontmatterPattern = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/;\nconst trailingSlashPattern = /\\/$/;\nconst frontmatterDescriptionPattern = /^description:\\s*(.+)$/m;\nconst frontmatterNamePattern = /^name:\\s*(.+)$/m;\n\nfunction joinPath(directory: string, name: string) {\n  return `${directory.replace(trailingSlashPattern, \"\")}/${name}`;\n}\n\nexport function parseSkillMetadata(\n  content: string\n): Omit<SkillMetadata, \"path\"> | null {\n  const match = content.match(frontmatterPattern);\n\n  if (!match) {\n    return null;\n  }\n\n  const frontmatter = match[1] ?? \"\";\n  const name = frontmatter.match(frontmatterNamePattern)?.[1]?.trim();\n  const description = frontmatter\n    .match(frontmatterDescriptionPattern)?.[1]\n    ?.trim();\n\n  if (!(name && description)) {\n    return null;\n  }\n\n  return {\n    description,\n    name,\n  };\n}\n\nexport function stripFrontmatter(content: string) {\n  const match = content.match(frontmatterPattern);\n\n  return match ? content.slice(match[0].length).trim() : content.trim();\n}\n\nexport async function discoverSkills(\n  sandbox: SkillsSandbox,\n  directories: string[]\n): Promise<SkillMetadata[]> {\n  const seen = new Set<string>();\n  const skills: SkillMetadata[] = [];\n\n  for (const directory of directories) {\n    const entries = (await sandbox.readdir(directory, {\n      withFileTypes: true,\n    })) as SkillDirent[];\n\n    for (const entry of entries) {\n      if (!entry.isDirectory()) {\n        continue;\n      }\n\n      const skillDirectory = joinPath(directory, entry.name);\n      const skillFile = joinPath(skillDirectory, \"SKILL.md\");\n\n      try {\n        const content = await sandbox.readFile(skillFile, \"utf-8\");\n        const metadata = parseSkillMetadata(content);\n\n        if (!metadata || seen.has(metadata.name.toLowerCase())) {\n          continue;\n        }\n\n        seen.add(metadata.name.toLowerCase());\n        skills.push({\n          ...metadata,\n          path: skillDirectory,\n        });\n      } catch {\n        // Ignore directories without a readable SKILL.md.\n      }\n    }\n  }\n\n  return skills.sort((left, right) => left.name.localeCompare(right.name));\n}\n\nexport async function loadSkill(\n  sandbox: SkillsSandbox,\n  skills: SkillMetadata[],\n  name: string\n): Promise<LoadedSkill> {\n  const skill = skills.find(\n    (candidate) => candidate.name.toLowerCase() === name.toLowerCase()\n  );\n\n  if (!skill) {\n    throw new Error(`Skill \"${name}\" was not found in the catalog.`);\n  }\n\n  const content = await sandbox.readFile(\n    joinPath(skill.path, \"SKILL.md\"),\n    \"utf-8\"\n  );\n\n  return {\n    content: stripFrontmatter(content),\n    name: skill.name,\n    skillDirectory: skill.path,\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/skill-catalog.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/vercel-sandbox.ts",
      "content": "import { readdir, readFile, stat } from \"node:fs/promises\";\nimport path, { posix as posixPath } from \"node:path\";\nimport { Sandbox } from \"@vercel/sandbox\";\nimport {\n  getSkillsAgentEnv,\n  getSkillsAgentSandboxSetupState,\n  getSkillsAgentSandboxTokenCredentials,\n  type SkillsAgentEnv,\n} from \"./env\";\n\nexport const VERCEL_SANDBOX_WORKSPACE_ROOT = process.cwd();\nexport const VERCEL_SANDBOX_PROJECT_ROOT = \"/vercel/sandbox/project\";\nexport const VERCEL_SANDBOX_SKILLS_ROOT =\n  `${VERCEL_SANDBOX_PROJECT_ROOT}/.agents/skills`;\nexport const VERCEL_SANDBOX_ARTIFACTS_ROOT =\n  `${VERCEL_SANDBOX_PROJECT_ROOT}/artifacts`;\nexport const VERCEL_SANDBOX_AGENTS_FILE =\n  `${VERCEL_SANDBOX_PROJECT_ROOT}/AGENTS.md`;\nconst VERCEL_SANDBOX_PYTHON_VERSION = \"3.13\";\nconst VERCEL_SANDBOX_PYTHON_PROJECT_NAME = \"skills-agent-sandbox\";\n\ninterface SandboxFs {\n  exists(path: string): Promise<boolean>;\n  mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n  readFile(path: string, encoding: BufferEncoding | \"utf-8\"): Promise<string>;\n  writeFile(\n    path: string,\n    content: string | Uint8Array,\n    encoding?: BufferEncoding | \"utf-8\"\n  ): Promise<void>;\n}\n\nexport interface VercelSandboxHandle {\n  fs: SandboxFs;\n  runCommand(options: {\n    args: string[];\n    cmd: string;\n    cwd: string;\n    sudo?: boolean;\n  }): Promise<{\n    exitCode: number;\n    stderr(): Promise<string>;\n    stdout(): Promise<string>;\n  }>;\n  stop(): Promise<unknown>;\n}\n\nexport type VercelSandboxFactory = (\n  sessionId: string\n) => Promise<VercelSandboxHandle>;\n\nexport interface VercelSandboxSession {\n  readonly artifactsRoot: string;\n  pathExists(targetPath: string): Promise<boolean>;\n  readonly projectRoot: string;\n  readFile(targetPath: string): Promise<string>;\n  runCommand(command: string): Promise<{\n    command: string;\n    exitCode: number;\n    stderr: string;\n    stdout: string;\n  }>;\n  readonly sessionId: string;\n  stop(): Promise<void>;\n  writeFile(\n    targetPath: string,\n    content: string\n  ): Promise<{\n    bytes: number;\n    path: string;\n  }>;\n}\n\nexport interface VercelSandboxSessionRegistry {\n  getSandbox(sessionId: string): Promise<VercelSandboxHandle>;\n  getSession(sessionId: string): VercelSandboxSession;\n  stopSession(sessionId: string): Promise<void>;\n}\n\ninterface SessionEntry {\n  sandboxPromise: Promise<VercelSandboxHandle> | null;\n  seededBaseWorkspace: boolean;\n}\n\nexport async function createVercelSandbox(\n  sessionId: string,\n  env: SkillsAgentEnv = getSkillsAgentEnv(),\n  options: {\n    ports?: number[];\n  } = {}\n): Promise<VercelSandboxHandle> {\n  const setupState = getSkillsAgentSandboxSetupState(env);\n\n  if (!setupState.isReady) {\n    throw new Error(setupState.issues.join(\" \"));\n  }\n\n  const baseOptions = {\n    name: sessionId,\n    persistent: true,\n    ports: options.ports,\n    runtime: setupState.runtime,\n    timeout: 300_000,\n  };\n\n  if (setupState.authMode === \"oidc\") {\n    try {\n      return (await Sandbox.get({\n        name: sessionId,\n      })) as unknown as VercelSandboxHandle;\n    } catch {\n      return (await Sandbox.create(\n        baseOptions\n      )) as unknown as VercelSandboxHandle;\n    }\n  }\n\n  const tokenCredentials = getSkillsAgentSandboxTokenCredentials(env);\n\n  try {\n    return (await Sandbox.get({\n      name: sessionId,\n      projectId: tokenCredentials.projectId,\n      teamId: tokenCredentials.teamId,\n      token: tokenCredentials.token,\n    })) as unknown as VercelSandboxHandle;\n  } catch {\n    return (await Sandbox.create({\n      ...baseOptions,\n      projectId: tokenCredentials.projectId,\n      teamId: tokenCredentials.teamId,\n      token: tokenCredentials.token,\n    })) as unknown as VercelSandboxHandle;\n  }\n}\n\nasync function pathExists(localPath: string) {\n  try {\n    await stat(localPath);\n    return true;\n  } catch (error) {\n    if (\n      typeof error === \"object\" &&\n      error !== null &&\n      \"code\" in error &&\n      error.code === \"ENOENT\"\n    ) {\n      return false;\n    }\n\n    throw error;\n  }\n}\n\nexport async function copyLocalPathToSandbox(\n  sandbox: VercelSandboxHandle,\n  localPath: string,\n  remotePath: string\n) {\n  const entry = await stat(localPath);\n\n  if (entry.isDirectory()) {\n    await sandbox.fs.mkdir(remotePath, { recursive: true });\n\n    for (const child of await readdir(localPath)) {\n      await copyLocalPathToSandbox(\n        sandbox,\n        path.join(localPath, child),\n        posixPath.join(remotePath, child)\n      );\n    }\n\n    return;\n  }\n\n  await sandbox.fs.mkdir(posixPath.dirname(remotePath), { recursive: true });\n  await sandbox.fs.writeFile(remotePath, await readFile(localPath));\n}\n\nfunction resolveSandboxPath(targetPath: string) {\n  if (targetPath.startsWith(\"/\")) {\n    return targetPath;\n  }\n\n  return posixPath.join(VERCEL_SANDBOX_PROJECT_ROOT, targetPath);\n}\n\nfunction shellQuote(value: string) {\n  return `'${value.replace(/'/g, `'\"'\"'`)}'`;\n}\n\nfunction formatSandboxError(error: unknown) {\n  if (!(error instanceof Error)) {\n    return String(error);\n  }\n\n  const details = [\n    \"response\" in error &&\n    typeof error.response === \"object\" &&\n    error.response &&\n    \"status\" in error.response &&\n    typeof error.response.status === \"number\"\n      ? `HTTP ${error.response.status}`\n      : null,\n    \"sandboxName\" in error && typeof error.sandboxName === \"string\"\n      ? `sandbox=${error.sandboxName}`\n      : null,\n    \"sessionId\" in error && typeof error.sessionId === \"string\"\n      ? `session=${error.sessionId}`\n      : null,\n    \"json\" in error && typeof error.json === \"object\" && error.json\n      ? `json=${JSON.stringify(error.json)}`\n      : null,\n    \"text\" in error && typeof error.text === \"string\" && error.text\n      ? `text=${error.text}`\n      : null,\n    error.message ? `message=${error.message}` : null,\n  ].filter(Boolean);\n\n  return details.join(\" | \") || error.message;\n}\n\nfunction createSandboxOperationError({\n  action,\n  error,\n  target,\n}: {\n  action: string;\n  error: unknown;\n  target: string;\n}) {\n  return new Error(\n    `Sandbox ${action} failed for ${target}. ${formatSandboxError(error)}`\n  );\n}\n\nfunction buildUvInstallCommand() {\n  return [\n    \"set -euo pipefail\",\n    \"if ! command -v curl >/dev/null 2>&1; then\",\n    \"  dnf install -y curl\",\n    \"fi\",\n    \"if ! command -v uv >/dev/null 2>&1; then\",\n    \"  curl --retry 1 --retry-delay 1 -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL=/usr/local/bin sh\",\n    \"fi\",\n    \"uv --version\",\n  ].join(\"\\n\");\n}\n\nfunction buildPythonProjectBootstrapCommand() {\n  return [\n    \"set -euo pipefail\",\n    \"command -v uv >/dev/null 2>&1 || { echo 'uv is not available after sandbox bootstrap.' >&2; exit 127; }\",\n    \"if [ -f pyproject.toml ] && [ -f .python-version ] && [ -d .venv ]; then\",\n    \"  uv --version\",\n    \"  exit 0\",\n    \"fi\",\n    `uv python install ${VERCEL_SANDBOX_PYTHON_VERSION}`,\n    \"if [ ! -f pyproject.toml ]; then\",\n    `  uv init --bare --name ${VERCEL_SANDBOX_PYTHON_PROJECT_NAME} --python ${VERCEL_SANDBOX_PYTHON_VERSION}`,\n    \"fi\",\n    `uv python pin ${VERCEL_SANDBOX_PYTHON_VERSION}`,\n    `uv venv .venv --python ${VERCEL_SANDBOX_PYTHON_VERSION} --allow-existing`,\n    \"uv run python --version\",\n  ].join(\"\\n\");\n}\n\nasync function runSandboxBootstrapCommand({\n  action,\n  command,\n  sandbox,\n  sudo,\n}: {\n  action: string;\n  command: string;\n  sandbox: VercelSandboxHandle;\n  sudo?: boolean;\n}) {\n  let result: Awaited<ReturnType<VercelSandboxHandle[\"runCommand\"]>>;\n\n  try {\n    const commandOptions: Parameters<VercelSandboxHandle[\"runCommand\"]>[0] = {\n      args: [\"-lc\", command],\n      cmd: \"bash\",\n      cwd: VERCEL_SANDBOX_PROJECT_ROOT,\n    };\n\n    if (sudo) {\n      commandOptions.sudo = true;\n    }\n\n    result = await sandbox.runCommand(commandOptions);\n  } catch (error) {\n    throw createSandboxOperationError({\n      action,\n      error,\n      target: VERCEL_SANDBOX_PROJECT_ROOT,\n    });\n  }\n\n  if (result.exitCode === 0) {\n    return;\n  }\n\n  const stderr = (await result.stderr()).trim();\n  const stdout = (await result.stdout()).trim();\n\n  throw new Error(\n    `Sandbox ${action} failed for ${VERCEL_SANDBOX_PROJECT_ROOT}. ${stderr || stdout || command}`\n  );\n}\n\nasync function bootstrapSandboxPythonProject(sandbox: VercelSandboxHandle) {\n  await runSandboxBootstrapCommand({\n    action: \"uv install\",\n    command: buildUvInstallCommand(),\n    sandbox,\n    sudo: true,\n  });\n  await runSandboxBootstrapCommand({\n    action: \"Python project bootstrap\",\n    command: buildPythonProjectBootstrapCommand(),\n    sandbox,\n  });\n}\n\nasync function writeSandboxFile(\n  sandbox: VercelSandboxHandle,\n  resolvedPath: string,\n  content: string\n) {\n  try {\n    await sandbox.fs.mkdir(posixPath.dirname(resolvedPath), {\n      recursive: true,\n    });\n    await sandbox.fs.writeFile(resolvedPath, content, \"utf-8\");\n    return;\n  } catch (primaryError) {\n    const base64Content = Buffer.from(content, \"utf-8\").toString(\"base64\");\n    const fallbackCommand = [\n      `mkdir -p ${shellQuote(posixPath.dirname(resolvedPath))}`,\n      `base64 -d > ${shellQuote(resolvedPath)} <<'__SKILLS_AGENT_CONTENT__'`,\n      base64Content,\n      \"__SKILLS_AGENT_CONTENT__\",\n    ].join(\"\\n\");\n\n    let fallbackResult: Awaited<ReturnType<VercelSandboxHandle[\"runCommand\"]>>;\n\n    try {\n      fallbackResult = await sandbox.runCommand({\n        args: [\"-lc\", fallbackCommand],\n        cmd: \"bash\",\n        cwd: VERCEL_SANDBOX_PROJECT_ROOT,\n      });\n    } catch (fallbackError) {\n      throw new Error(\n        [\n          `Sandbox writeFile failed for ${resolvedPath}.`,\n          `File API error: ${formatSandboxError(primaryError)}`,\n          `Shell fallback error: ${formatSandboxError(fallbackError)}`,\n        ].join(\" \")\n      );\n    }\n\n    if (fallbackResult.exitCode === 0) {\n      return;\n    }\n\n    throw new Error(\n      [\n        \"Failed to write a sandbox file.\",\n        `File API error: ${formatSandboxError(primaryError)}`,\n        `Shell fallback error: ${(await fallbackResult.stderr()).trim() || \"unknown error\"}`,\n      ].join(\" \")\n    );\n  }\n}\n\nexport function createVercelSandboxSessionRegistry({\n  createSandbox,\n  workspaceRoot = VERCEL_SANDBOX_WORKSPACE_ROOT,\n}: {\n  createSandbox: VercelSandboxFactory;\n  workspaceRoot?: string;\n}): VercelSandboxSessionRegistry {\n  const entries = new Map<string, SessionEntry>();\n\n  const stopSession = async (sessionId: string) => {\n    const entry = entries.get(sessionId);\n\n    if (!entry) {\n      return;\n    }\n\n    if (!entry.sandboxPromise) {\n      entries.delete(sessionId);\n      return;\n    }\n\n    let sandbox: VercelSandboxHandle;\n\n    try {\n      sandbox = await entry.sandboxPromise;\n    } catch {\n      entries.delete(sessionId);\n      return;\n    }\n\n    await sandbox.stop();\n    entries.delete(sessionId);\n  };\n\n  const getOrCreateEntry = (sessionId: string) => {\n    const existingEntry = entries.get(sessionId);\n\n    if (existingEntry) {\n      return existingEntry;\n    }\n\n    const createdEntry: SessionEntry = {\n      sandboxPromise: null,\n      seededBaseWorkspace: false,\n    };\n    entries.set(sessionId, createdEntry);\n    return createdEntry;\n  };\n\n  const ensureSandbox = async (sessionId: string, entry: SessionEntry) => {\n    if (!entry.sandboxPromise) {\n      entry.sandboxPromise = (async () => {\n        let lastError: unknown;\n\n        for (let attempt = 0; attempt < 2; attempt += 1) {\n          try {\n            const sandbox = await createSandbox(sessionId);\n\n            if (!entry.seededBaseWorkspace) {\n              await sandbox.fs.mkdir(VERCEL_SANDBOX_PROJECT_ROOT, {\n                recursive: true,\n              });\n              await sandbox.fs.mkdir(VERCEL_SANDBOX_SKILLS_ROOT, {\n                recursive: true,\n              });\n              await sandbox.fs.mkdir(VERCEL_SANDBOX_ARTIFACTS_ROOT, {\n                recursive: true,\n              });\n\n              const agentsPath = path.join(\n                /*turbopackIgnore: true*/ workspaceRoot,\n                \"AGENTS.md\"\n              );\n\n              if (await pathExists(agentsPath)) {\n                await copyLocalPathToSandbox(\n                  sandbox,\n                  agentsPath,\n                  VERCEL_SANDBOX_AGENTS_FILE\n                );\n              }\n\n              await bootstrapSandboxPythonProject(sandbox);\n              entry.seededBaseWorkspace = true;\n            }\n\n            return sandbox;\n          } catch (error) {\n            lastError = error;\n            entry.seededBaseWorkspace = false;\n          }\n        }\n\n        throw lastError;\n      })().catch((error) => {\n        entry.sandboxPromise = null;\n        throw error;\n      });\n    }\n\n    return await entry.sandboxPromise;\n  };\n\n  return {\n    getSandbox(sessionId) {\n      const entry = getOrCreateEntry(sessionId);\n      return ensureSandbox(sessionId, entry);\n    },\n    getSession(sessionId) {\n      const entry = getOrCreateEntry(sessionId);\n\n      return {\n        artifactsRoot: VERCEL_SANDBOX_ARTIFACTS_ROOT,\n        projectRoot: VERCEL_SANDBOX_PROJECT_ROOT,\n        sessionId,\n        async pathExists(targetPath) {\n          const sandbox = await ensureSandbox(sessionId, entry);\n          return sandbox.fs.exists(resolveSandboxPath(targetPath));\n        },\n        async readFile(targetPath) {\n          const sandbox = await ensureSandbox(sessionId, entry);\n          const resolvedPath = resolveSandboxPath(targetPath);\n\n          try {\n            return await sandbox.fs.readFile(resolvedPath, \"utf-8\");\n          } catch (error) {\n            throw createSandboxOperationError({\n              action: \"readFile\",\n              error,\n              target: resolvedPath,\n            });\n          }\n        },\n        async runCommand(command) {\n          const sandbox = await ensureSandbox(sessionId, entry);\n          let result: Awaited<ReturnType<VercelSandboxHandle[\"runCommand\"]>>;\n\n          try {\n            result = await sandbox.runCommand({\n              args: [\"-lc\", command],\n              cmd: \"bash\",\n              cwd: VERCEL_SANDBOX_PROJECT_ROOT,\n            });\n          } catch (error) {\n            throw createSandboxOperationError({\n              action: \"bash\",\n              error,\n              target: command,\n            });\n          }\n\n          return {\n            command,\n            exitCode: result.exitCode,\n            stderr: await result.stderr(),\n            stdout: await result.stdout(),\n          };\n        },\n        async stop() {\n          await stopSession(sessionId);\n        },\n        async writeFile(targetPath, content) {\n          const sandbox = await ensureSandbox(sessionId, entry);\n          const resolvedPath = resolveSandboxPath(targetPath);\n\n          await writeSandboxFile(sandbox, resolvedPath, content);\n\n          return {\n            bytes: Buffer.byteLength(content, \"utf-8\"),\n            path: resolvedPath,\n          };\n        },\n      };\n    },\n    stopSession,\n  };\n}\n\nlet sharedRegistry: VercelSandboxSessionRegistry | null = null;\n\nexport function getSharedVercelSandboxSessionRegistry(\n  env: SkillsAgentEnv = getSkillsAgentEnv()\n) {\n  sharedRegistry ??= createVercelSandboxSessionRegistry({\n    createSandbox: (sessionId) => createVercelSandbox(sessionId, env),\n  });\n\n  return sharedRegistry;\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/vercel-sandbox.ts"
    },
    {
      "path": "registry/skills-agent/lib/skills-agent/server/workspace.ts",
      "content": "import { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getSkillsAgentEnv } from \"./env\";\nimport { SKILLS_AGENT_WORKSPACE_ROOT } from \"./local-skill-catalog\";\nimport {\n  createSkillsAgentOfficialTools,\n  type SkillsAgentOfficialTools,\n} from \"./official-tools\";\nimport {\n  getSharedSkillsAgentSessionRegistry,\n  type SkillsAgentSession,\n} from \"./sandbox\";\nimport type { SkillMetadata } from \"./skill-catalog\";\n\ntype DemoEnv = Record<string, string | undefined>;\n\nexport interface VisibleSkillMetadata {\n  description: string;\n  name: string;\n  path: string;\n}\n\nexport interface SkillsAgentWorkspace {\n  readonly artifactsRoot: string;\n  readonly projectRoot: string;\n  readonly session: SkillsAgentSession;\n  readonly toolset: SkillsAgentOfficialTools;\n  readonly visibleSkillCatalog: VisibleSkillMetadata[];\n  readonly visibleSkillCatalogText: string;\n}\n\nexport const SKILLS_AGENT_AGENTS_PATH = path.join(\n  SKILLS_AGENT_WORKSPACE_ROOT,\n  \"AGENTS.md\"\n);\nexport const SKILLS_AGENT_SKILLS_DIRECTORY = path.join(\n  SKILLS_AGENT_WORKSPACE_ROOT,\n  \".agents/skills\"\n);\nconst defaultAgentsContent = [\n  \"# Skills Agent Consumer Workspace\",\n  \"\",\n  \"This project can install repo-local skills under `.agents/skills`.\",\n  \"Use loaded skill instructions as the source of truth, keep generated drafts under `artifacts/` unless a skill names a canonical path, and inspect optional repository files before reading them.\",\n].join(\"\\n\");\n\nfunction isMissingFileError(error: unknown) {\n  return (\n    typeof error === \"object\" &&\n    error !== null &&\n    \"code\" in error &&\n    error.code === \"ENOENT\"\n  );\n}\n\nasync function readAgentsContent(readAgentsFile: typeof readFile) {\n  try {\n    return await readAgentsFile(SKILLS_AGENT_AGENTS_PATH, \"utf-8\");\n  } catch (error) {\n    if (isMissingFileError(error)) {\n      return defaultAgentsContent;\n    }\n\n    throw error;\n  }\n}\n\nexport function toVisibleSkillCatalog(\n  skills: SkillMetadata[]\n): VisibleSkillMetadata[] {\n  return skills.map(({ description, name, path }) => ({\n    description,\n    name,\n    path,\n  }));\n}\n\nexport function formatVisibleSkillCatalog(skills: VisibleSkillMetadata[]) {\n  return skills\n    .map(\n      (skill) => `- ${skill.name}: ${skill.description} (path: ${skill.path})`\n    )\n    .join(\"\\n\");\n}\n\nexport async function createSkillsAgentWorkspace(\n  {\n    env = getSkillsAgentEnv(),\n    sessionId,\n    skills,\n  }: {\n    env?: DemoEnv;\n    sessionId: string;\n    skills: SkillMetadata[];\n  },\n  dependencies: {\n    createOfficialTools?: typeof createSkillsAgentOfficialTools;\n    readAgentsFile?: typeof readFile;\n  } = {}\n): Promise<SkillsAgentWorkspace> {\n  const readAgentsFile = dependencies.readAgentsFile ?? readFile;\n  const createOfficialTools =\n    dependencies.createOfficialTools ?? createSkillsAgentOfficialTools;\n  const session = (\n    await getSharedSkillsAgentSessionRegistry(env)\n  ).getSession(sessionId);\n  const visibleSkillCatalog = toVisibleSkillCatalog(skills);\n  const agentsContent = await readAgentsContent(readAgentsFile);\n  await session.writeFile(\"AGENTS.md\", agentsContent);\n  const toolset = await createOfficialTools({\n    agentsContent,\n    projectRoot: session.projectRoot,\n    session,\n    skills,\n  });\n\n  return {\n    artifactsRoot: session.artifactsRoot,\n    projectRoot: session.projectRoot,\n    session,\n    toolset,\n    visibleSkillCatalog,\n    visibleSkillCatalogText: formatVisibleSkillCatalog(visibleSkillCatalog),\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/skills-agent/server/workspace.ts"
    },
    {
      "path": "registry/skills-agent/.agents/skills/grill-with-docs/SKILL.md",
      "content": "---\nname: grill-with-docs\ndescription: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.\n---\n\n<what-to-do>\n\nInterview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.\n\nAsk the questions one at a time, waiting for feedback on each question before continuing.\n\nIf a question can be answered by exploring the codebase, explore the codebase instead.\n\n</what-to-do>\n\n<supporting-info>\n\n## Domain awareness\n\nDuring codebase exploration, also look for existing documentation:\n\n### File structure\n\nMost repos have a single context:\n\n```\n/\n├── CONTEXT.md\n├── docs/\n│   └── adr/\n│       ├── 0001-event-sourced-orders.md\n│       └── 0002-postgres-for-write-model.md\n└── src/\n```\n\nIf a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:\n\n```\n/\n├── CONTEXT-MAP.md\n├── docs/\n│   └── adr/                          ← system-wide decisions\n├── src/\n│   ├── ordering/\n│   │   ├── CONTEXT.md\n│   │   └── docs/adr/                 ← context-specific decisions\n│   └── billing/\n│       ├── CONTEXT.md\n│       └── docs/adr/\n```\n\nCreate files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.\n\n## During the session\n\n### Challenge against the glossary\n\nWhen the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. \"Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?\"\n\n### Sharpen fuzzy language\n\nWhen the user uses vague or overloaded terms, propose a precise canonical term. \"You're saying 'account' — do you mean the Customer or the User? Those are different things.\"\n\n### Discuss concrete scenarios\n\nWhen domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.\n\n### Cross-reference with code\n\nWhen the user states how something works, check whether the code agrees. If you find a contradiction, surface it: \"Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?\"\n\n### Update CONTEXT.md inline\n\nWhen a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).\n\n`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.\n\n### Offer ADRs sparingly\n\nOnly offer to create an ADR when all three are true:\n\n1. **Hard to reverse** — the cost of changing your mind later is meaningful\n2. **Surprising without context** — a future reader will wonder \"why did they do it this way?\"\n3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons\n\nIf any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).\n\n</supporting-info>\n",
      "type": "registry:file",
      "target": ".agents/skills/grill-with-docs/SKILL.md"
    },
    {
      "path": "registry/skills-agent/.agents/skills/grill-with-docs/ADR-FORMAT.md",
      "content": "# ADR Format\n\nADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.\n\nCreate the `docs/adr/` directory lazily — only when the first ADR is needed.\n\n## Template\n\n```md\n# {Short title of the decision}\n\n{1-3 sentences: what's the context, what did we decide, and why.}\n```\n\nThat's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.\n\n## Optional sections\n\nOnly include these when they add genuine value. Most ADRs won't need them.\n\n- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited\n- **Considered Options** — only when the rejected alternatives are worth remembering\n- **Consequences** — only when non-obvious downstream effects need to be called out\n\n## Numbering\n\nScan `docs/adr/` for the highest existing number and increment by one.\n\n## When to offer an ADR\n\nAll three of these must be true:\n\n1. **Hard to reverse** — the cost of changing your mind later is meaningful\n2. **Surprising without context** — a future reader will look at the code and wonder \"why on earth did they do it this way?\"\n3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons\n\nIf a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond \"we did the obvious thing.\"\n\n### What qualifies\n\n- **Architectural shape.** \"We're using a monorepo.\" \"The write model is event-sourced, the read model is projected into Postgres.\"\n- **Integration patterns between contexts.** \"Ordering and Billing communicate via domain events, not synchronous HTTP.\"\n- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.\n- **Boundary and scope decisions.** \"Customer data is owned by the Customer context; other contexts reference it by ID only.\" The explicit no-s are as valuable as the yes-s.\n- **Deliberate deviations from the obvious path.** \"We're using manual SQL instead of an ORM because X.\" Anything where a reasonable reader would assume the opposite. These stop the next engineer from \"fixing\" something that was deliberate.\n- **Constraints not visible in the code.** \"We can't use AWS because of compliance requirements.\" \"Response times must be under 200ms because of the partner API contract.\"\n- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.\n",
      "type": "registry:file",
      "target": ".agents/skills/grill-with-docs/ADR-FORMAT.md"
    },
    {
      "path": "registry/skills-agent/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md",
      "content": "# CONTEXT.md Format\n\n## Structure\n\n```md\n# {Context Name}\n\n{One or two sentence description of what this context is and why it exists.}\n\n## Language\n\n**Order**:\n{A one or two sentence description of the term}\n_Avoid_: Purchase, transaction\n\n**Invoice**:\nA request for payment sent to a customer after delivery.\n_Avoid_: Bill, payment request\n\n**Customer**:\nA person or organization that places orders.\n_Avoid_: Client, buyer, account\n```\n\n## Rules\n\n- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.\n- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in \"Flagged ambiguities\" with a clear resolution.\n- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does.\n- **Show relationships.** Use bold term names and express cardinality where obvious.\n- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.\n- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.\n- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts.\n\n## Single vs multi-context repos\n\n**Single context (most repos):** One `CONTEXT.md` at the repo root.\n\n**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:\n\n```md\n# Context Map\n\n## Contexts\n\n- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders\n- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments\n- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping\n\n## Relationships\n\n- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking\n- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices\n- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`\n```\n\nThe skill infers which structure applies:\n\n- If `CONTEXT-MAP.md` exists, read it to find contexts\n- If only a root `CONTEXT.md` exists, single context\n- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved\n\nWhen multiple contexts exist, infer which one the current topic relates to. If unclear, ask.\n",
      "type": "registry:file",
      "target": ".agents/skills/grill-with-docs/CONTEXT-FORMAT.md"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/SKILL.md",
      "content": "---\nname: skill-creator\ndescription: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.\nmetadata:\n  short-description: Create or update a skill\n---\n\n# Skill Creator\n\nThis skill provides guidance for creating effective skills.\n\n## About Skills\n\nSkills are modular, self-contained folders that extend Codex's capabilities by providing\nspecialized knowledge, workflows, and tools. Think of them as \"onboarding guides\" for specific\ndomains or tasks—they transform Codex from a general-purpose agent into a specialized agent\nequipped with procedural knowledge that no model can fully possess.\n\n### What Skills Provide\n\n1. Specialized workflows - Multi-step procedures for specific domains\n2. Tool integrations - Instructions for working with specific file formats or APIs\n3. Domain expertise - Company-specific knowledge, schemas, business logic\n4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks\n\n## Core Principles\n\n### Concise is Key\n\nThe context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.\n\n**Default assumption: Codex is already very smart.** Only add context Codex doesn't already have. Challenge each piece of information: \"Does Codex really need this explanation?\" and \"Does this paragraph justify its token cost?\"\n\nPrefer concise examples over verbose explanations.\n\n### Set Appropriate Degrees of Freedom\n\nMatch the level of specificity to the task's fragility and variability:\n\n**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.\n\n**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.\n\n**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.\n\nThink of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).\n\n### Protect Validation Integrity\n\nYou may use subagents during iteration to validate whether a skill works on realistic tasks or whether a suspected problem is real. This is most useful when you want an independent pass on the skill's behavior, outputs, or failure modes after a revision.  Only do this when it is possible to start new subagents.\n\nWhen using subagents for validation, treat that as an evaluation surface. The goal is to learn whether the skill generalizes, not whether another agent can reconstruct the answer from leaked context.\n\nPrefer raw artifacts such as example prompts, outputs, diffs, logs, or traces. Give the minimum task-local context needed to perform the validation. Avoid passing the intended answer, suspected bug, intended fix, or your prior conclusions unless the validation explicitly requires them.\n\n### Anatomy of a Skill\n\nEvery skill consists of a required SKILL.md file and optional bundled resources:\n\n```\nskill-name/\n├── SKILL.md (required)\n│   ├── YAML frontmatter metadata (required)\n│   │   ├── name: (required)\n│   │   └── description: (required)\n│   └── Markdown instructions (required)\n├── agents/ (recommended)\n│   └── openai.yaml - UI metadata for skill lists and chips\n└── Bundled Resources (optional)\n    ├── scripts/          - Executable code (Python/Bash/etc.)\n    ├── references/       - Documentation intended to be loaded into context as needed\n    └── assets/           - Files used in output (templates, icons, fonts, etc.)\n```\n\n#### SKILL.md (required)\n\nEvery SKILL.md consists of:\n\n- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.\n- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).\n\n#### Agents metadata (recommended)\n\n- UI-facing metadata for skill lists and chips\n- Read references/openai_yaml.md before generating values and follow its descriptions and constraints\n- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill\n- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`\n- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale\n- Only include other optional interface fields (icons, brand color) if explicitly provided\n- See references/openai_yaml.md for field definitions and examples\n\n#### Bundled Resources (optional)\n\n##### Scripts (`scripts/`)\n\nExecutable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.\n\n- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed\n- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks\n- **Benefits**: Token efficient, deterministic, may be executed without loading into context\n- **Note**: Scripts may still need to be read by Codex for patching or environment-specific adjustments\n\n##### References (`references/`)\n\nDocumentation and reference material intended to be loaded as needed into context to inform Codex's process and thinking.\n\n- **When to include**: For documentation that Codex should reference while working\n- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications\n- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides\n- **Benefits**: Keeps SKILL.md lean, loaded only when Codex determines it's needed\n- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md\n- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.\n\n##### Assets (`assets/`)\n\nFiles not intended to be loaded into context, but rather used within the output Codex produces.\n\n- **When to include**: When the skill needs files that will be used in the final output\n- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography\n- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified\n- **Benefits**: Separates output resources from documentation, enables Codex to use files without loading them into context\n\n#### What to Not Include in a Skill\n\nA skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:\n\n- README.md\n- INSTALLATION_GUIDE.md\n- QUICK_REFERENCE.md\n- CHANGELOG.md\n- etc.\n\nThe skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.\n\n### Progressive Disclosure Design Principle\n\nSkills use a three-level loading system to manage context efficiently:\n\n1. **Metadata (name + description)** - Always in context (~100 words)\n2. **SKILL.md body** - When skill triggers (<5k words)\n3. **Bundled resources** - As needed by Codex (Unlimited because scripts can be executed without reading into context window)\n\n#### Progressive Disclosure Patterns\n\nKeep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.\n\n**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.\n\n**Pattern 1: High-level guide with references**\n\n```markdown\n# PDF Processing\n\n## Quick start\n\nExtract text with pdfplumber:\n[code example]\n\n## Advanced features\n\n- **Form filling**: See [FORMS.md](FORMS.md) for complete guide\n- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods\n- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns\n```\n\nCodex loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.\n\n**Pattern 2: Domain-specific organization**\n\nFor Skills with multiple domains, organize content by domain to avoid loading irrelevant context:\n\n```\nbigquery-skill/\n├── SKILL.md (overview and navigation)\n└── reference/\n    ├── finance.md (revenue, billing metrics)\n    ├── sales.md (opportunities, pipeline)\n    ├── product.md (API usage, features)\n    └── marketing.md (campaigns, attribution)\n```\n\nWhen a user asks about sales metrics, Codex only reads sales.md.\n\nSimilarly, for skills supporting multiple frameworks or variants, organize by variant:\n\n```\ncloud-deploy/\n├── SKILL.md (workflow + provider selection)\n└── references/\n    ├── aws.md (AWS deployment patterns)\n    ├── gcp.md (GCP deployment patterns)\n    └── azure.md (Azure deployment patterns)\n```\n\nWhen the user chooses AWS, Codex only reads aws.md.\n\n**Pattern 3: Conditional details**\n\nShow basic content, link to advanced content:\n\n```markdown\n# DOCX Processing\n\n## Creating documents\n\nUse docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).\n\n## Editing documents\n\nFor simple edits, modify the XML directly.\n\n**For tracked changes**: See [REDLINING.md](REDLINING.md)\n**For OOXML details**: See [OOXML.md](OOXML.md)\n```\n\nCodex reads REDLINING.md or OOXML.md only when the user needs those features.\n\n**Important guidelines:**\n\n- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.\n- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Codex can see the full scope when previewing.\n\n## Skill Creation Process\n\nSkill creation involves these steps:\n\n1. Understand the skill with concrete examples\n2. Plan reusable skill contents (scripts, references, assets)\n3. Initialize the skill (run init_skill.py)\n4. Edit the skill (implement resources and write SKILL.md)\n5. Validate the skill (run quick_validate.py)\n6. Iterate based on real usage and forward-test complex skills.\n\nFollow these steps in order, skipping only if there is a clear reason why they are not applicable.\n\n### Skill Naming\n\n- Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., \"Plan Mode\" -> `plan-mode`).\n- When generating names, generate a name under 64 characters (letters, digits, hyphens).\n- Prefer short, verb-led phrases that describe the action.\n- Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`).\n- Name the skill folder exactly after the skill name.\n\n### Step 1: Understanding the Skill with Concrete Examples\n\nSkip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.\n\nTo create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.\n\nFor example, when building an image-editor skill, relevant questions include:\n\n- \"What functionality should the image-editor skill support? Editing, rotating, anything else?\"\n- \"Can you give some examples of how this skill would be used?\"\n- \"I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?\"\n- \"What would a user say that should trigger this skill?\"\n- \"Where should I create this skill? If you do not have a preference, I will place it in `$CODEX_HOME/skills` (or `~/.codex/skills` when `CODEX_HOME` is unset) so Codex can discover it automatically.\"\n\nTo avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.\n\nConclude this step when there is a clear sense of the functionality the skill should support.\n\n### Step 2: Planning the Reusable Skill Contents\n\nTo turn concrete examples into an effective skill, analyze each example by:\n\n1. Considering how to execute on the example from scratch\n2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly\n\nExample: When building a `pdf-editor` skill to handle queries like \"Help me rotate this PDF,\" the analysis shows:\n\n1. Rotating a PDF requires re-writing the same code each time\n2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill\n\nExample: When designing a `frontend-webapp-builder` skill for queries like \"Build me a todo app\" or \"Build me a dashboard to track my steps,\" the analysis shows:\n\n1. Writing a frontend webapp requires the same boilerplate HTML/React each time\n2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill\n\nExample: When building a `big-query` skill to handle queries like \"How many users have logged in today?\" the analysis shows:\n\n1. Querying BigQuery requires re-discovering the table schemas and relationships each time\n2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill\n\nTo establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.\n\n### Step 3: Initializing the Skill\n\nAt this point, it is time to actually create the skill.\n\nSkip this step only if the skill being developed already exists. In this case, continue to the next step.\n\nBefore running `init_skill.py`, ask where the user wants the skill created. If they do not specify a location, default to `$CODEX_HOME/skills`; when `CODEX_HOME` is unset, fall back to `~/.codex/skills` so the skill is auto-discovered.\n\nWhen creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.\n\nUsage:\n\n```bash\nscripts/init_skill.py <skill-name> --path <output-directory> [--resources scripts,references,assets] [--examples]\n```\n\nExamples:\n\n```bash\nscripts/init_skill.py my-skill --path \"${CODEX_HOME:-$HOME/.codex}/skills\"\nscripts/init_skill.py my-skill --path \"${CODEX_HOME:-$HOME/.codex}/skills\" --resources scripts,references\nscripts/init_skill.py my-skill --path ~/work/skills --resources scripts --examples\n```\n\nThe script:\n\n- Creates the skill directory at the specified path\n- Generates a SKILL.md template with proper frontmatter and TODO placeholders\n- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`\n- Optionally creates resource directories based on `--resources`\n- Optionally adds example files when `--examples` is set\n\nAfter initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.\n\nGenerate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:\n\n```bash\nscripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value\n```\n\nOnly include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.\n\n### Step 4: Edit the Skill\n\nWhen editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.\n\nAfter substantial revisions, or if the skill is particularly tricky, you should use subagents to forward-test the skill on realistic tasks or artifacts. When doing so, pass the artifact under validation rather than your diagnosis of what is wrong, and keep the prompt generic enough that success depends on transferable reasoning rather than hidden ground truth.\n\n#### Start with Reusable Skill Contents\n\nTo begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.\n\nAdded scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.\n\nIf you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.\n\n#### Update SKILL.md\n\n**Writing Guidelines:** Always use imperative/infinitive form.\n\n##### Frontmatter\n\nWrite the YAML frontmatter with `name` and `description`:\n\n- `name`: The skill name\n- `description`: This is the primary triggering mechanism for your skill, and helps Codex understand when to use the skill.\n  - Include both what the Skill does and specific triggers/contexts for when to use it.\n  - Include all \"when to use\" information here - Not in the body. The body is only loaded after triggering, so \"When to Use This Skill\" sections in the body are not helpful to Codex.\n  - Example description for a `docx` skill: \"Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks\"\n\nDo not include any other fields in YAML frontmatter.\n\n##### Body\n\nWrite instructions for using the skill and its bundled resources.\n\n### Step 5: Validate the Skill\n\nOnce development of the skill is complete, validate the skill folder to catch basic issues early:\n\n```bash\nscripts/quick_validate.py <path/to/skill-folder>\n```\n\nThe validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.\n\n### Step 6: Iterate\n\nAfter testing the skill, you may detect the skill is complex enough that it requires forward-testing; or users may request improvements.\n\nUser testing often this happens right after using the skill, with fresh context of how the skill performed.\n\n**Forward-testing and iteration workflow:**\n\n1. Use the skill on real tasks\n2. Notice struggles or inefficiencies\n3. Identify how SKILL.md or bundled resources should be updated\n4. Implement changes and test again\n5. Forward-test if it is reasonable and appropriate\n\n## Forward-testing\n\nTo forward-test, launch subagents as a way to stress test the skill with minimal context.\nSubagents should *not* know that they are being asked to test the skill.  They should be treated as\nan agent asked to perform a task by the user.  Prompts to subagents should look like:\n  `Use $skill-x at /path/to/skill-x to solve problem y`\nNot:\n  `Review the skill at /path/to/skill-x; pretend a user asks you to...`\n\nDecision rule for forward-testing:\n  - Err on the side of forward-testing\n  - Ask for approval if you think there's a risk that forward-testing would:\n    * take a long time,\n    * require additional approvals from the user, or\n    * modify live production systems\n\n  In these cases, show the user your proposed prompt and request (1) a yes/no decision, and\n  (2) any suggested modifictions.\n\nConsiderations when forward-testing:\n   - use fresh threads for independent passes\n   - pass the skill, and a request in a similar way the user would.\n   - pass raw artifacts, not your conclusions\n   - avoid showing expected answers or intended fixes\n   - rebuild context from source artifacts after each iteration\n   - review the subagent's output and reasoning and emitted artifacts\n   - avoid leaving artifacts the agent can find on disk between iterations;\n     clean up subagents' artifacts to avoid additional contamination.\n\nIf forward-testing only succeeds when subagents see leaked context, tighten the skill or the\nforward-testing setup before trusting the result.\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/SKILL.md"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/agents/openai.yaml",
      "content": "interface:\n  display_name: \"Skill Creator\"\n  short_description: \"Create or update a skill\"\n  icon_small: \"./assets/skill-creator-small.svg\"\n  icon_large: \"./assets/skill-creator.png\"\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/agents/openai.yaml"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/assets/skill-creator-small.svg",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n  <path fill=\"#0D0D0D\" d=\"M12.03 4.113a3.612 3.612 0 0 1 5.108 5.108l-6.292 6.29c-.324.324-.56.561-.791.752l-.235.176c-.205.14-.422.261-.65.36l-.229.093a4.136 4.136 0 0 1-.586.16l-.764.134-2.394.4c-.142.024-.294.05-.423.06-.098.007-.232.01-.378-.026l-.149-.05a1.081 1.081 0 0 1-.521-.474l-.046-.093a1.104 1.104 0 0 1-.075-.527c.01-.129.035-.28.06-.422l.398-2.394c.1-.602.162-.987.295-1.35l.093-.23c.1-.228.22-.445.36-.65l.176-.235c.19-.232.428-.467.751-.79l6.292-6.292Zm-5.35 7.232c-.35.35-.534.535-.66.688l-.11.147a2.67 2.67 0 0 0-.24.433l-.062.154c-.08.22-.124.462-.232 1.112l-.398 2.394-.001.001h.003l2.393-.399.717-.126a2.63 2.63 0 0 0 .394-.105l.154-.063a2.65 2.65 0 0 0 .433-.24l.147-.11c.153-.126.339-.31.688-.66l4.988-4.988-3.227-3.226-4.987 4.988Zm9.517-6.291a2.281 2.281 0 0 0-3.225 0l-.364.362 3.226 3.227.363-.364c.89-.89.89-2.334 0-3.225ZM4.583 1.783a.3.3 0 0 1 .294.241c.117.585.347 1.092.707 1.48.357.385.859.668 1.549.783a.3.3 0 0 1 0 .592c-.69.115-1.192.398-1.549.783-.315.34-.53.77-.657 1.265l-.05.215a.3.3 0 0 1-.588 0c-.117-.585-.347-1.092-.707-1.48-.357-.384-.859-.668-1.549-.783a.3.3 0 0 1 0-.592c.69-.115 1.192-.398 1.549-.783.36-.388.59-.895.707-1.48l.015-.05a.3.3 0 0 1 .279-.19Z\"/>\n</svg>\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/assets/skill-creator-small.svg"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/assets/skill-creator.png",
      "content": "�PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000d\u0000\u0000\u0000d\b\u0006\u0000\u0000\u0000p�T\u0000\u0000\u0000\tpHYs\u0000\u0000\u000b\u0013\u0000\u0000\u000b\u0013\u0001\u0000��\u0018\u0000\u0000\u0000\u0001sRGB\u0000��\u001c�\u0000\u0000\u0000\u0004gAMA\u0000\u0000��\u000b�a\u0005\u0000\u0000\u0005�IDATx\u0001��]k\u001cU\u001c��ߙ�m�6i֦mBhd��\"\u00161\"B�M\u0013�P!�\u0005o�3�h\u0011�0x�M\u0013�\n\u0016�\u000f�\"Eھ\u0000I|\u0001%A/�\b�Z%\u0017>d�56Zj�4�fw3�9�\u001c�y�9�3�/��f��~�Ϝ��$\u0000EQ\u0014EQ\u0014EQ\u0014EQ\u0014EQ�B\f�����fPJu��t�\u001bu�\u0000�M&�\u001c�\u001b�E\b%\u001e�|�\u000b���(6d�s�M��/�Ь\t�q��>`\"�\u0012\u000b�?y+\u000b�c\u0018��\fܦ�Kе@`\u0012\u0007�/\u000e�qn\u0014��]�\u0007�� ;��\u0010|,Q ��p\u0006Ek��*\u0003���\u0019Ñ\u000b}�{֗}���\u0014\b�(ߜ�ϧG�S�\u0000\t\fC�?��G޿\b\u001f�=���d�����[)#� �x/��ܛ�X��!\u0002C�4{2��9w\u0007m�|\u0005�XD��\"�?�e�\u0015�\t��\u0010�\u0017\u001b\u001a��3�`�R\b�4��3�P,'d-Fe�Lʒ�\u0010\u001bP{e\u001f�\t�\nC\u0014ʤh%�}I�@�Ð\u0005����P,6�,>5�)���\u001b��\u0012�8�M��/��V,&D`�\u001b�;p/�fL�����\u0005;)�\f\u0014�y\u0010�!_���<\"���\u001dP��A�b�\"��P͂l�!�:\n��Y�n�!�.JB@�bȪ�R2rP��@�b��Gaf�_��b�BF��b5\u0001�\u0015C\u0016\u001aʮ��P,� ~a�\u0002Ga�`��L@�H���!+�,�\u0007�4����P�\u001c��\"\u000b\u0012\u0014F����\u0012��1\u001c����(\u001c\u001f��KJ�+Y$A����\u0017�S6�(̄�\u000f�c:\"VX\u00182ݚC}~\u001c�ꞃ�5l{\u001fK;�0��\u0000�ޜ\u0006�����\u0018KZ��Խ\t�E\n$l\f�7\u0014.1L�Pd@��!SAY��d6^��7\fQ$ޠ�6FeE�\u00157�j��u����eu�����OH�0D\u000e'%\u0010��㣊E\rC�\rJ`\u0018��F��*�l\u0013�@1ʏ�*\u0014u\f�\u001a��1D���k\u0005����9u���1D���\"���4t�\u0010\n\r�0�\u0015\n\ba8/p\u0010�pW� ���@\bC�@@\bC=�A\b�[��cȿ;I\u0018\u001e��-��\u0013���-�@�ԕa��\u000e`'�,I\u0018\"_�!��+�`|���E�^Z>�z�\t�\u0010�3!\f\u0015�\u0002�\u0013��$\u0011C�\u0019�<\u001d��\u001b�P��!�>!���25�$c�<���WU�P��!�6!����Wr�B\u0018�y\u0003ћ\u001d\u001e �5\na<�\u001b\b��C7F!��)�,�?�vw��(��>�\t)�\\bȖQ��\u001f\bc��?\u0006�+�}��֓{v���0��6`�k�rv$��j\u001aC�\f������k�vv>�(�P�z�ݝ~��<�H}\u001f���߾��G>�8U�h\u0006k9[>�X,0D� ���\n+\u0002(��\u0010)m���lڲ�Ay~��\"�^�\rU�|�\nC�4!�Ri��*LJ�0DJ rs��\u0010Qb�!R\u0005�lvY\b(��\u0010)��\u0015�V�\u0007�\u0012k\f�����\u0002@�=���*k�\nk��Z}���9��\u0010w\f��\t�h��U�'Eo��C\u0017b?\u00192� ����\u000f\u0015�P�,,>�\u001e;��\u000ew��\u0007��Ph���9J\u0019b\b�v�\u001dy�\u0003$,ן�\u0012�\u0010]��*\u0014\nuP�྽8�Ƌ�ӕ��,\u0018�Y�[�kzӘ�0�\u0004��A��������3.of��r�����g[��N\u001d͡�#��͑��?����3h�O6B1���ۗ��i\u0012�?�\u001c\u001b\u001b�'�A�>J*6_r�e\u0018�x�鉧(��(��(��(�����\u001fAI�ҟ�j�\u0000\u0000\u0000\u0000IEND�B`�",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/assets/skill-creator.png"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/license.txt",
      "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/license.txt"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/references/openai_yaml.md",
      "content": "# openai.yaml fields (full example + descriptions)\n\n`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.\n\n## Full example\n\n```yaml\ninterface:\n  display_name: \"Optional user-facing name\"\n  short_description: \"Optional user-facing description\"\n  icon_small: \"./assets/small-400px.png\"\n  icon_large: \"./assets/large-logo.svg\"\n  brand_color: \"#3B82F6\"\n  default_prompt: \"Optional surrounding prompt to use the skill with\"\n\ndependencies:\n  tools:\n    - type: \"mcp\"\n      value: \"github\"\n      description: \"GitHub MCP server\"\n      transport: \"streamable_http\"\n      url: \"https://api.githubcopilot.com/mcp/\"\n\npolicy:\n  allow_implicit_invocation: true\n```\n\n## Field descriptions and constraints\n\nTop-level constraints:\n\n- Quote all string values.\n- Keep keys unquoted.\n- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., \"Use $skill-name-here to draft a concise weekly status update.\").\n\n- `interface.display_name`: Human-facing title shown in UI skill lists and chips.\n- `interface.short_description`: Human-facing short UI blurb (25–64 chars) for quick scanning.\n- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.\n- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.\n- `interface.brand_color`: Hex color used for UI accents (e.g., badges).\n- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.\n- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.\n- `dependencies.tools[].value`: Identifier of the tool or dependency.\n- `dependencies.tools[].description`: Human-readable explanation of the dependency.\n- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.\n- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.\n- `policy.allow_implicit_invocation`: When false, the skill is not injected into\n  the model context by default, but can still be invoked explicitly via `$skill`.\n  Defaults to true.\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/references/openai_yaml.md"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/scripts/generate_openai_yaml.py",
      "content": "#!/usr/bin/env python3\n\"\"\"\nOpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.\n\nUsage:\n    generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]\n\"\"\"\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\nACRONYMS = {\n    \"GH\",\n    \"MCP\",\n    \"API\",\n    \"CI\",\n    \"CLI\",\n    \"LLM\",\n    \"PDF\",\n    \"PR\",\n    \"UI\",\n    \"URL\",\n    \"SQL\",\n}\n\nBRANDS = {\n    \"openai\": \"OpenAI\",\n    \"openapi\": \"OpenAPI\",\n    \"github\": \"GitHub\",\n    \"pagerduty\": \"PagerDuty\",\n    \"datadog\": \"DataDog\",\n    \"sqlite\": \"SQLite\",\n    \"fastapi\": \"FastAPI\",\n}\n\nSMALL_WORDS = {\"and\", \"or\", \"to\", \"up\", \"with\"}\n\nALLOWED_INTERFACE_KEYS = {\n    \"display_name\",\n    \"short_description\",\n    \"icon_small\",\n    \"icon_large\",\n    \"brand_color\",\n    \"default_prompt\",\n}\n\n\ndef yaml_quote(value):\n    escaped = value.replace(\"\\\\\", \"\\\\\\\\\").replace('\"', '\\\\\"').replace(\"\\n\", \"\\\\n\")\n    return f'\"{escaped}\"'\n\n\ndef format_display_name(skill_name):\n    words = [word for word in skill_name.split(\"-\") if word]\n    formatted = []\n    for index, word in enumerate(words):\n        lower = word.lower()\n        upper = word.upper()\n        if upper in ACRONYMS:\n            formatted.append(upper)\n            continue\n        if lower in BRANDS:\n            formatted.append(BRANDS[lower])\n            continue\n        if index > 0 and lower in SMALL_WORDS:\n            formatted.append(lower)\n            continue\n        formatted.append(word.capitalize())\n    return \" \".join(formatted)\n\n\ndef generate_short_description(display_name):\n    description = f\"Help with {display_name} tasks\"\n\n    if len(description) < 25:\n        description = f\"Help with {display_name} tasks and workflows\"\n    if len(description) < 25:\n        description = f\"Help with {display_name} tasks with guidance\"\n\n    if len(description) > 64:\n        description = f\"Help with {display_name}\"\n    if len(description) > 64:\n        description = f\"{display_name} helper\"\n    if len(description) > 64:\n        description = f\"{display_name} tools\"\n    if len(description) > 64:\n        suffix = \" helper\"\n        max_name_length = 64 - len(suffix)\n        trimmed = display_name[:max_name_length].rstrip()\n        description = f\"{trimmed}{suffix}\"\n    if len(description) > 64:\n        description = description[:64].rstrip()\n\n    if len(description) < 25:\n        description = f\"{description} workflows\"\n        if len(description) > 64:\n            description = description[:64].rstrip()\n\n    return description\n\n\ndef read_frontmatter_name(skill_dir):\n    skill_md = Path(skill_dir) / \"SKILL.md\"\n    if not skill_md.exists():\n        print(f\"[ERROR] SKILL.md not found in {skill_dir}\")\n        return None\n    content = skill_md.read_text()\n    match = re.match(r\"^---\\n(.*?)\\n---\", content, re.DOTALL)\n    if not match:\n        print(\"[ERROR] Invalid SKILL.md frontmatter format.\")\n        return None\n    frontmatter_text = match.group(1)\n\n    import yaml\n\n    try:\n        frontmatter = yaml.safe_load(frontmatter_text)\n    except yaml.YAMLError as exc:\n        print(f\"[ERROR] Invalid YAML frontmatter: {exc}\")\n        return None\n    if not isinstance(frontmatter, dict):\n        print(\"[ERROR] Frontmatter must be a YAML dictionary.\")\n        return None\n    name = frontmatter.get(\"name\", \"\")\n    if not isinstance(name, str) or not name.strip():\n        print(\"[ERROR] Frontmatter 'name' is missing or invalid.\")\n        return None\n    return name.strip()\n\n\ndef parse_interface_overrides(raw_overrides):\n    overrides = {}\n    optional_order = []\n    for item in raw_overrides:\n        if \"=\" not in item:\n            print(f\"[ERROR] Invalid interface override '{item}'. Use key=value.\")\n            return None, None\n        key, value = item.split(\"=\", 1)\n        key = key.strip()\n        value = value.strip()\n        if not key:\n            print(f\"[ERROR] Invalid interface override '{item}'. Key is empty.\")\n            return None, None\n        if key not in ALLOWED_INTERFACE_KEYS:\n            allowed = \", \".join(sorted(ALLOWED_INTERFACE_KEYS))\n            print(f\"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}\")\n            return None, None\n        overrides[key] = value\n        if key not in (\"display_name\", \"short_description\") and key not in optional_order:\n            optional_order.append(key)\n    return overrides, optional_order\n\n\ndef write_openai_yaml(skill_dir, skill_name, raw_overrides):\n    overrides, optional_order = parse_interface_overrides(raw_overrides)\n    if overrides is None:\n        return None\n\n    display_name = overrides.get(\"display_name\") or format_display_name(skill_name)\n    short_description = overrides.get(\"short_description\") or generate_short_description(display_name)\n\n    if not (25 <= len(short_description) <= 64):\n        print(\n            \"[ERROR] short_description must be 25-64 characters \"\n            f\"(got {len(short_description)}).\"\n        )\n        return None\n\n    interface_lines = [\n        \"interface:\",\n        f\"  display_name: {yaml_quote(display_name)}\",\n        f\"  short_description: {yaml_quote(short_description)}\",\n    ]\n\n    for key in optional_order:\n        value = overrides.get(key)\n        if value is not None:\n            interface_lines.append(f\"  {key}: {yaml_quote(value)}\")\n\n    agents_dir = Path(skill_dir) / \"agents\"\n    agents_dir.mkdir(parents=True, exist_ok=True)\n    output_path = agents_dir / \"openai.yaml\"\n    output_path.write_text(\"\\n\".join(interface_lines) + \"\\n\")\n    print(f\"[OK] Created agents/openai.yaml\")\n    return output_path\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Create agents/openai.yaml for a skill directory.\",\n    )\n    parser.add_argument(\"skill_dir\", help=\"Path to the skill directory\")\n    parser.add_argument(\n        \"--name\",\n        help=\"Skill name override (defaults to SKILL.md frontmatter)\",\n    )\n    parser.add_argument(\n        \"--interface\",\n        action=\"append\",\n        default=[],\n        help=\"Interface override in key=value format (repeatable)\",\n    )\n    args = parser.parse_args()\n\n    skill_dir = Path(args.skill_dir).resolve()\n    if not skill_dir.exists():\n        print(f\"[ERROR] Skill directory not found: {skill_dir}\")\n        sys.exit(1)\n    if not skill_dir.is_dir():\n        print(f\"[ERROR] Path is not a directory: {skill_dir}\")\n        sys.exit(1)\n\n    skill_name = args.name or read_frontmatter_name(skill_dir)\n    if not skill_name:\n        sys.exit(1)\n\n    result = write_openai_yaml(skill_dir, skill_name, args.interface)\n    if result:\n        sys.exit(0)\n    sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/scripts/generate_openai_yaml.py"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/scripts/init_skill.py",
      "content": "#!/usr/bin/env python3\n\"\"\"\nSkill Initializer - Creates a new skill from template\n\nUsage:\n    init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples] [--interface key=value]\n\nExamples:\n    init_skill.py my-new-skill --path skills/public\n    init_skill.py my-new-skill --path skills/public --resources scripts,references\n    init_skill.py my-api-helper --path skills/private --resources scripts --examples\n    init_skill.py custom-skill --path /custom/location\n    init_skill.py my-skill --path skills/public --interface short_description=\"Short UI label\"\n\"\"\"\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\nfrom generate_openai_yaml import write_openai_yaml\n\nMAX_SKILL_NAME_LENGTH = 64\nALLOWED_RESOURCES = {\"scripts\", \"references\", \"assets\"}\n\nSKILL_TEMPLATE = \"\"\"---\nname: {skill_name}\ndescription: \"TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.\"\n---\n\n# {skill_title}\n\n## Overview\n\n[TODO: 1-2 sentences explaining what this skill enables]\n\n## Structuring This Skill\n\n[TODO: Choose the structure that best fits this skill's purpose. Common patterns:\n\n**1. Workflow-Based** (best for sequential processes)\n- Works well when there are clear step-by-step procedures\n- Example: DOCX skill with \"Workflow Decision Tree\" -> \"Reading\" -> \"Creating\" -> \"Editing\"\n- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2...\n\n**2. Task-Based** (best for tool collections)\n- Works well when the skill offers different operations/capabilities\n- Example: PDF skill with \"Quick Start\" -> \"Merge PDFs\" -> \"Split PDFs\" -> \"Extract Text\"\n- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...\n\n**3. Reference/Guidelines** (best for standards or specifications)\n- Works well for brand guidelines, coding standards, or requirements\n- Example: Brand styling with \"Brand Guidelines\" -> \"Colors\" -> \"Typography\" -> \"Features\"\n- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage...\n\n**4. Capabilities-Based** (best for integrated systems)\n- Works well when the skill provides multiple interrelated features\n- Example: Product Management with \"Core Capabilities\" -> numbered capability list\n- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...\n\nPatterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).\n\nDelete this entire \"Structuring This Skill\" section when done - it's just guidance.]\n\n## [TODO: Replace with the first main section based on chosen structure]\n\n[TODO: Add content here. See examples in existing skills:\n- Code samples for technical skills\n- Decision trees for complex workflows\n- Concrete examples with realistic user requests\n- References to scripts/templates/references as needed]\n\n## Resources (optional)\n\nCreate only the resource directories this skill actually needs. Delete this section if no resources are required.\n\n### scripts/\nExecutable code (Python/Bash/etc.) that can be run directly to perform specific operations.\n\n**Examples from other skills:**\n- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation\n- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing\n\n**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.\n\n**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.\n\n### references/\nDocumentation and reference material intended to be loaded into context to inform Codex's process and thinking.\n\n**Examples from other skills:**\n- Product management: `communication.md`, `context_building.md` - detailed workflow guides\n- BigQuery: API reference documentation and query examples\n- Finance: Schema documentation, company policies\n\n**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.\n\n### assets/\nFiles not intended to be loaded into context, but rather used within the output Codex produces.\n\n**Examples from other skills:**\n- Brand styling: PowerPoint template files (.pptx), logo files\n- Frontend builder: HTML/React boilerplate project directories\n- Typography: Font files (.ttf, .woff2)\n\n**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.\n\n---\n\n**Not every skill requires all three types of resources.**\n\"\"\"\n\nEXAMPLE_SCRIPT = '''#!/usr/bin/env python3\n\"\"\"\nExample helper script for {skill_name}\n\nThis is a placeholder script that can be executed directly.\nReplace with actual implementation or delete if not needed.\n\nExample real scripts from other skills:\n- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields\n- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images\n\"\"\"\n\ndef main():\n    print(\"This is an example script for {skill_name}\")\n    # TODO: Add actual script logic here\n    # This could be data processing, file conversion, API calls, etc.\n\nif __name__ == \"__main__\":\n    main()\n'''\n\nEXAMPLE_REFERENCE = \"\"\"# Reference Documentation for {skill_title}\n\nThis is a placeholder for detailed reference documentation.\nReplace with actual reference content or delete if not needed.\n\nExample real reference docs from other skills:\n- product-management/references/communication.md - Comprehensive guide for status updates\n- product-management/references/context_building.md - Deep-dive on gathering context\n- bigquery/references/ - API references and query examples\n\n## When Reference Docs Are Useful\n\nReference docs are ideal for:\n- Comprehensive API documentation\n- Detailed workflow guides\n- Complex multi-step processes\n- Information too lengthy for main SKILL.md\n- Content that's only needed for specific use cases\n\n## Structure Suggestions\n\n### API Reference Example\n- Overview\n- Authentication\n- Endpoints with examples\n- Error codes\n- Rate limits\n\n### Workflow Guide Example\n- Prerequisites\n- Step-by-step instructions\n- Common patterns\n- Troubleshooting\n- Best practices\n\"\"\"\n\nEXAMPLE_ASSET = \"\"\"# Example Asset File\n\nThis placeholder represents where asset files would be stored.\nReplace with actual asset files (templates, images, fonts, etc.) or delete if not needed.\n\nAsset files are NOT intended to be loaded into context, but rather used within\nthe output Codex produces.\n\nExample asset files from other skills:\n- Brand guidelines: logo.png, slides_template.pptx\n- Frontend builder: hello-world/ directory with HTML/React boilerplate\n- Typography: custom-font.ttf, font-family.woff2\n- Data: sample_data.csv, test_dataset.json\n\n## Common Asset Types\n\n- Templates: .pptx, .docx, boilerplate directories\n- Images: .png, .jpg, .svg, .gif\n- Fonts: .ttf, .otf, .woff, .woff2\n- Boilerplate code: Project directories, starter files\n- Icons: .ico, .svg\n- Data files: .csv, .json, .xml, .yaml\n\nNote: This is a text placeholder. Actual assets can be any file type.\n\"\"\"\n\n\ndef normalize_skill_name(skill_name):\n    \"\"\"Normalize a skill name to lowercase hyphen-case.\"\"\"\n    normalized = skill_name.strip().lower()\n    normalized = re.sub(r\"[^a-z0-9]+\", \"-\", normalized)\n    normalized = normalized.strip(\"-\")\n    normalized = re.sub(r\"-{2,}\", \"-\", normalized)\n    return normalized\n\n\ndef title_case_skill_name(skill_name):\n    \"\"\"Convert hyphenated skill name to Title Case for display.\"\"\"\n    return \" \".join(word.capitalize() for word in skill_name.split(\"-\"))\n\n\ndef parse_resources(raw_resources):\n    if not raw_resources:\n        return []\n    resources = [item.strip() for item in raw_resources.split(\",\") if item.strip()]\n    invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})\n    if invalid:\n        allowed = \", \".join(sorted(ALLOWED_RESOURCES))\n        print(f\"[ERROR] Unknown resource type(s): {', '.join(invalid)}\")\n        print(f\"   Allowed: {allowed}\")\n        sys.exit(1)\n    deduped = []\n    seen = set()\n    for resource in resources:\n        if resource not in seen:\n            deduped.append(resource)\n            seen.add(resource)\n    return deduped\n\n\ndef create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):\n    for resource in resources:\n        resource_dir = skill_dir / resource\n        resource_dir.mkdir(exist_ok=True)\n        if resource == \"scripts\":\n            if include_examples:\n                example_script = resource_dir / \"example.py\"\n                example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))\n                example_script.chmod(0o755)\n                print(\"[OK] Created scripts/example.py\")\n            else:\n                print(\"[OK] Created scripts/\")\n        elif resource == \"references\":\n            if include_examples:\n                example_reference = resource_dir / \"api_reference.md\"\n                example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))\n                print(\"[OK] Created references/api_reference.md\")\n            else:\n                print(\"[OK] Created references/\")\n        elif resource == \"assets\":\n            if include_examples:\n                example_asset = resource_dir / \"example_asset.txt\"\n                example_asset.write_text(EXAMPLE_ASSET)\n                print(\"[OK] Created assets/example_asset.txt\")\n            else:\n                print(\"[OK] Created assets/\")\n\n\ndef init_skill(skill_name, path, resources, include_examples, interface_overrides):\n    \"\"\"\n    Initialize a new skill directory with template SKILL.md.\n\n    Args:\n        skill_name: Name of the skill\n        path: Path where the skill directory should be created\n        resources: Resource directories to create\n        include_examples: Whether to create example files in resource directories\n\n    Returns:\n        Path to created skill directory, or None if error\n    \"\"\"\n    # Determine skill directory path\n    skill_dir = Path(path).resolve() / skill_name\n\n    # Check if directory already exists\n    if skill_dir.exists():\n        print(f\"[ERROR] Skill directory already exists: {skill_dir}\")\n        return None\n\n    # Create skill directory\n    try:\n        skill_dir.mkdir(parents=True, exist_ok=False)\n        print(f\"[OK] Created skill directory: {skill_dir}\")\n    except Exception as e:\n        print(f\"[ERROR] Error creating directory: {e}\")\n        return None\n\n    # Create SKILL.md from template\n    skill_title = title_case_skill_name(skill_name)\n    skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)\n\n    skill_md_path = skill_dir / \"SKILL.md\"\n    try:\n        skill_md_path.write_text(skill_content)\n        print(\"[OK] Created SKILL.md\")\n    except Exception as e:\n        print(f\"[ERROR] Error creating SKILL.md: {e}\")\n        return None\n\n    # Create agents/openai.yaml\n    try:\n        result = write_openai_yaml(skill_dir, skill_name, interface_overrides)\n        if not result:\n            return None\n    except Exception as e:\n        print(f\"[ERROR] Error creating agents/openai.yaml: {e}\")\n        return None\n\n    # Create resource directories if requested\n    if resources:\n        try:\n            create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)\n        except Exception as e:\n            print(f\"[ERROR] Error creating resource directories: {e}\")\n            return None\n\n    # Print next steps\n    print(f\"\\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}\")\n    print(\"\\nNext steps:\")\n    print(\"1. Edit SKILL.md to complete the TODO items and update the description\")\n    if resources:\n        if include_examples:\n            print(\"2. Customize or delete the example files in scripts/, references/, and assets/\")\n        else:\n            print(\"2. Add resources to scripts/, references/, and assets/ as needed\")\n    else:\n        print(\"2. Create resource directories only if needed (scripts/, references/, assets/)\")\n    print(\"3. Update agents/openai.yaml if the UI metadata should differ\")\n    print(\"4. Run the validator when ready to check the skill structure\")\n    print(\n        \"5. Forward-test complex skills with realistic user requests to ensure they work as intended\"\n    )\n\n    return skill_dir\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Create a new skill directory with a SKILL.md template.\",\n    )\n    parser.add_argument(\"skill_name\", help=\"Skill name (normalized to hyphen-case)\")\n    parser.add_argument(\"--path\", required=True, help=\"Output directory for the skill\")\n    parser.add_argument(\n        \"--resources\",\n        default=\"\",\n        help=\"Comma-separated list: scripts,references,assets\",\n    )\n    parser.add_argument(\n        \"--examples\",\n        action=\"store_true\",\n        help=\"Create example files inside the selected resource directories\",\n    )\n    parser.add_argument(\n        \"--interface\",\n        action=\"append\",\n        default=[],\n        help=\"Interface override in key=value format (repeatable)\",\n    )\n    args = parser.parse_args()\n\n    raw_skill_name = args.skill_name\n    skill_name = normalize_skill_name(raw_skill_name)\n    if not skill_name:\n        print(\"[ERROR] Skill name must include at least one letter or digit.\")\n        sys.exit(1)\n    if len(skill_name) > MAX_SKILL_NAME_LENGTH:\n        print(\n            f\"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). \"\n            f\"Maximum is {MAX_SKILL_NAME_LENGTH} characters.\"\n        )\n        sys.exit(1)\n    if skill_name != raw_skill_name:\n        print(f\"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.\")\n\n    resources = parse_resources(args.resources)\n    if args.examples and not resources:\n        print(\"[ERROR] --examples requires --resources to be set.\")\n        sys.exit(1)\n\n    path = args.path\n\n    print(f\"Initializing skill: {skill_name}\")\n    print(f\"   Location: {path}\")\n    if resources:\n        print(f\"   Resources: {', '.join(resources)}\")\n        if args.examples:\n            print(\"   Examples: enabled\")\n    else:\n        print(\"   Resources: none (create as needed)\")\n    print()\n\n    result = init_skill(skill_name, path, resources, args.examples, args.interface)\n\n    if result:\n        sys.exit(0)\n    else:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/scripts/init_skill.py"
    },
    {
      "path": "registry/skills-agent/.agents/skills/skill-creator/scripts/quick_validate.py",
      "content": "#!/usr/bin/env python3\n\"\"\"\nQuick validation script for skills - minimal version\n\"\"\"\n\nimport re\nimport sys\nfrom pathlib import Path\n\nimport yaml\n\nMAX_SKILL_NAME_LENGTH = 64\n\n\ndef validate_skill(skill_path):\n    \"\"\"Basic validation of a skill\"\"\"\n    skill_path = Path(skill_path)\n\n    skill_md = skill_path / \"SKILL.md\"\n    if not skill_md.exists():\n        return False, \"SKILL.md not found\"\n\n    content = skill_md.read_text()\n    if not content.startswith(\"---\"):\n        return False, \"No YAML frontmatter found\"\n\n    match = re.match(r\"^---\\n(.*?)\\n---\", content, re.DOTALL)\n    if not match:\n        return False, \"Invalid frontmatter format\"\n\n    frontmatter_text = match.group(1)\n\n    try:\n        frontmatter = yaml.safe_load(frontmatter_text)\n        if not isinstance(frontmatter, dict):\n            return False, \"Frontmatter must be a YAML dictionary\"\n    except yaml.YAMLError as e:\n        return False, f\"Invalid YAML in frontmatter: {e}\"\n\n    allowed_properties = {\"name\", \"description\", \"license\", \"allowed-tools\", \"metadata\"}\n\n    unexpected_keys = set(frontmatter.keys()) - allowed_properties\n    if unexpected_keys:\n        allowed = \", \".join(sorted(allowed_properties))\n        unexpected = \", \".join(sorted(unexpected_keys))\n        return (\n            False,\n            f\"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}\",\n        )\n\n    if \"name\" not in frontmatter:\n        return False, \"Missing 'name' in frontmatter\"\n    if \"description\" not in frontmatter:\n        return False, \"Missing 'description' in frontmatter\"\n\n    name = frontmatter.get(\"name\", \"\")\n    if not isinstance(name, str):\n        return False, f\"Name must be a string, got {type(name).__name__}\"\n    name = name.strip()\n    if name:\n        if not re.match(r\"^[a-z0-9-]+$\", name):\n            return (\n                False,\n                f\"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)\",\n            )\n        if name.startswith(\"-\") or name.endswith(\"-\") or \"--\" in name:\n            return (\n                False,\n                f\"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens\",\n            )\n        if len(name) > MAX_SKILL_NAME_LENGTH:\n            return (\n                False,\n                f\"Name is too long ({len(name)} characters). \"\n                f\"Maximum is {MAX_SKILL_NAME_LENGTH} characters.\",\n            )\n\n    description = frontmatter.get(\"description\", \"\")\n    if not isinstance(description, str):\n        return False, f\"Description must be a string, got {type(description).__name__}\"\n    description = description.strip()\n    if description:\n        if \"<\" in description or \">\" in description:\n            return False, \"Description cannot contain angle brackets (< or >)\"\n        if len(description) > 1024:\n            return (\n                False,\n                f\"Description is too long ({len(description)} characters). Maximum is 1024 characters.\",\n            )\n\n    return True, \"Skill is valid!\"\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) != 2:\n        print(\"Usage: python quick_validate.py <skill_directory>\")\n        sys.exit(1)\n\n    valid, message = validate_skill(sys.argv[1])\n    print(message)\n    sys.exit(0 if valid else 1)\n",
      "type": "registry:file",
      "target": ".agents/skills/skill-creator/scripts/quick_validate.py"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-5-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the skills-agent page, node runtime chat route, client workspace, local sandbox-backed skill tools, and the two primary skill packages under .agents/skills so the browser happy path works after AI Gateway env setup. Add Vercel Sandbox credentials only when you want remote sandbox execution.",
  "type": "registry:block"
}
