Skip to content

fix(server): wait for plugin activation before listing skills - #51681

Closed
dbpolito wants to merge 1 commit into
anomalyco:devfrom
dbpolito:skill-list-wait
Closed

dbpolito wants to merge 1 commit into
anomalyco:devfrom
dbpolito:skill-list-wait

Conversation

@dbpolito

@dbpolito dbpolito commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #51680

Related: #51282, #35556

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Waits for plugin activation before listing skills, so the first request includes registered skills.

How did you verify your code works?

  • Added packages/server/test/skill.test.ts for the initial skill list.
  • A live server with the generated SDK client returned [] before the fix and registered skills after.

Screenshots / recordings

Not applicable.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@thdxr
thdxr changed the base branch from v2 to dev October 10, 2026 19:20
@CBEPX

CBEPX commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Agent work: stage finished

Client: codex · Session: codex-opencode-upstream-comment-20261020-1519 · Claim: ddfe11e7-553c-4473-bc56-85ed61e6fc7e

Mode: write · Started: 2026-10-20T15:20:26+03:00 · Updated: 2026-10-20T15:56:07+03:00

Stage: Recovery: Prior claim and abort requests completed; no child executors remain; abort the owned stopped claim with qualified marker recovery

Scope in this repository: read-only or external scope

@CBEPX

CBEPX commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

I reproduced #51680 on v2.1.0 (60c68d145a14127ce633e26aa5fdfb882a7baa6d). A cold standalone /api/skill request returned zero entries with exit code 0. A local candidate returned all 137 expected skill names without prior model or plugin requests. Both runs sent stdout to regular files.

This PR waits through Plugin.awaitActivation. Could it use the existing server awaitPlugins helper instead? That helper bounds the wait to 5 seconds and returns ServiceUnavailableError. The skill protocol group would also need that error declaration, followed by the normal client and OpenAPI generators.

Additional regression coverage would help distinguish a settled empty registry from activation in progress:

  • A plugin registers one skill, remains pending, then registers a second skill. The request must wait for settlement.
  • A settled registry with no skills returns an empty list.
  • Stalled activation returns typed HTTP 503. A scoped TestClock avoids a real 5-second wait.

A local five-file candidate covers those cases. Its three new tests fail on the original handler and protocol declarations. With the candidate, the focused tests pass. 10 repetitions give 30 passes. The server suite gives 90 passes, 1 skip, and no failures. The protocol suite gives 9 passes. Server and protocol typechecks and builds pass.

These results cover the local candidate, not PR head 81fdfeaa9020656ca3769054ab3f85f6afb1508d. Local validation used Bun 1.4.3. The repository specifies 1.4.2. Root lint passes, but root typecheck is blocked by missing @tsconfig/node22 in the scoped dependency install. The candidate applies to dev at d8148796dc74a342c96c7b4febf03dc664e1058c. Tests and builds ran on v2.1.0. Full workspace checks and CI on dev remain required.

Activation settlement includes failed plugins. It does not certify that every plugin is healthy. The installed CLI remains unchanged.

@CBEPX

CBEPX commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Follow-up to my earlier comment, with qualification from 23–24 October 2026. The Bun pin mismatch and incomplete root-check coverage from that comment are resolved for the local candidates below.

Candidate base Bun Patch SHA256
v2.1.2, a517687fde98a7329ada3ae644a01a9fc6c4e99e 1.4.2 258f5e54223ea4912c1a5b4bef07ad0398a78f9609b9f3fc55b2067f001af092
dev, d93568f3344e1a93297d56bc1dc71892104856d0 1.4.4 d1dbc82781115160634c65815c0fdda78b899db9fd24551218c0b568132e3467

Both candidates use the existing bounded awaitPlugins helper and declare ServiceUnavailableError in the skill protocol group. Client and OpenAPI artifacts came from the normal generators.

On 23 October, each candidate passed four focused tests, nine protocol tests, and ten regression repetitions totaling 30 passes. Server suites returned 90 passes for v2.1.2 and 95 for dev, each with one skip and zero failures. Root bun run check passed lint and 34 typechecks with zero cached tasks. Server and protocol builds passed. The server suite retained an existing MaxListenersExceededWarning in test/sse-failure.test.ts.

On 24 October, I checked source archives, all five candidate files, pinned Bun executable hashes, and 62 retained evidence bindings. I repeated these checks on both exact bases:

  • Two unchanged HTTP regressions fail on the original handler/protocol and pass with the candidate. They cover a pending partial registry, a settled empty registry, and typed HTTP 503 for stalled activation with TestClock.
  • On a cold start, the original source CLI handler returns zero skills for api --standalone GET /api/skill. The candidate returns OpenCode, Report, and source-sentinel. Without the fixture, it returns the two built-in skills. Stdout goes to regular files. No prior model/plugin request or inference occurs.

The fresh CLI uses isolated HOME/XDG paths, disables project and Claude discovery, and loads the fixture from its isolated configuration directory. This prevents ancestor discovery from including user skills. The cold/warm HTTP test and full suites were not repeated on 24 October. Their results above remain dated 23 October. This three-skill fixture is separate from the earlier 137-name check.

These results cover the tested local candidates, not PR head 81fdfeaa9020656ca3769054ab3f85f6afb1508d or current dev. Activation settlement includes failed plugins and does not certify plugin health. The installed CLI remains unchanged. All observed test processes stopped, but process polling does not prove exhaustive descendant containment.

Generated baseline drift is separate from this fix. Regeneration without the candidate adds Session.StructuredError.retryAfterMs on v2.1.2. On dev, it adds /api/experimental/client-mcp and /api/experimental/client-mcp/token. Relative to each regenerated baseline, the candidate adds only skill GET 503 and its declared client status. The complete patches retain that unrelated generated drift for reviewer inspection.

The dev patch and manifest below make the dev candidate self-contained. The v2.1.2 patch is not included in this comment. Its results provide a separate historical comparison. It remains a candidate for this existing PR. CI and validation on the eventual integrated head remain required.

Exact dev candidate manifest
{
  "commit": "d93568f3344e1a93297d56bc1dc71892104856d0",
  "bun": "1.4.4",
  "files": [
    {
      "path": "packages/server/src/handlers/skill.ts",
      "baseline_sha256": "428dc0167217bb47fb9bdcc883dcef6803a92b93e6e9ad1ccb1614ffb71bd5dc",
      "candidate_sha256": "3004e1ae9df238c98f43304144455b16464041478cd27718856d35576120a49d"
    },
    {
      "path": "packages/protocol/src/groups/skill.ts",
      "baseline_sha256": "18d47e5dc4cc2ec598ec37cf0222eee6f6527c38cc71f861869618b6e6541b0c",
      "candidate_sha256": "0c0dec82703907d85462a49688631bc921f4c6215f5b5f7eb336c04d76c83516"
    },
    {
      "path": "packages/server/test/skill.test.ts",
      "baseline_sha256": null,
      "candidate_sha256": "868f096864561de268b554e41271ada2042627bef56f965851ff10c693f79864"
    },
    {
      "path": "packages/client/src/promise/generated/client.ts",
      "baseline_sha256": "4924b56aae80d5fa3fa62fde60519f3284f29a943c8b35511998fcccaac81511",
      "candidate_sha256": "5fe3b362cb5b0c2b4c7911f4f513c1e5b10e07101d61fee119479322c5fa53b3"
    },
    {
      "path": "packages/protocol/openapi.json",
      "baseline_sha256": "c04f3cf77629247f47fc0a6fa4a8aeba689531894e9aa6c4b2a6e1b0b63c99eb",
      "candidate_sha256": "3c9cb4f4cea4dcd074ccfe7583102d7e32b0f668612c5f5a938b4451747e1492"
    }
  ],
  "patch_sha256": "d1dbc82781115160634c65815c0fdda78b899db9fd24551218c0b568132e3467",
  "patch_bytes": 17091
}
Exact dev candidate patch (17,091 bytes before Markdown wrapping)
diff --git a/packages/server/src/handlers/skill.ts b/packages/server/src/handlers/skill.ts
--- a/packages/server/src/handlers/skill.ts
+++ b/packages/server/src/handlers/skill.ts
@@ -1,8 +1,15 @@
 import { Skill } from "@opencode/core/skill"
+import { Effect } from "effect"
 import { HttpApiBuilder } from "effect/http-api"
 import { Api } from "../api"
-import { response } from "../location"
+import { awaitPlugins, response } from "../location"
 
 export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) =>
-  handlers.handle("skill.list", () => response(Skill.Service.use((skill) => skill.list()))),
+  handlers.handle(
+    "skill.list",
+    Effect.fn(function* () {
+      yield* awaitPlugins
+      return yield* response(Skill.Service.use((skill) => skill.list()))
+    }),
+  ),
 )
diff --git a/packages/protocol/src/groups/skill.ts b/packages/protocol/src/groups/skill.ts
--- a/packages/protocol/src/groups/skill.ts
+++ b/packages/protocol/src/groups/skill.ts
@@ -2,6 +2,7 @@
 import { Location } from "@opencode/schema/location"
 import { Schema } from "effect"
 import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api"
+import { ServiceUnavailableError } from "../errors.js"
 import { LocationQuery, locationQueryOpenApi } from "./location.js"
 
 export const SkillGroup = HttpApiGroup.make("server.skill")
@@ -9,6 +10,7 @@
     HttpApiEndpoint.get("skill.list", "/api/skill", {
       query: LocationQuery,
       success: Location.response(Schema.Array(Skill.Info)),
+      error: ServiceUnavailableError,
     })
       .annotateMerge(locationQueryOpenApi)
       .annotateMerge(
diff --git a/packages/server/test/skill.test.ts b/packages/server/test/skill.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/server/test/skill.test.ts
@@ -0,0 +1,150 @@
+import fs from "node:fs/promises"
+import path from "node:path"
+import { expect } from "bun:test"
+import { Skill } from "@opencode/core/skill"
+import { LocationServiceMap } from "@opencode/core/location-services"
+import { SdkPlugins } from "@opencode/core/plugin/sdk"
+import { define } from "@opencode/plugin/effect/plugin"
+import { Location } from "@opencode/schema/location"
+import { AbsolutePath } from "@opencode/schema/schema"
+import { Global } from "@opencode/util/global"
+import { Context, Deferred, Effect, Fiber, Layer } from "effect"
+import { HttpEffect, HttpRouter, HttpServer } from "effect/http"
+import { tempGlobalLayer } from "../../core/test/fixture/global"
+import { tmpdir } from "../../core/test/fixture/tmpdir"
+import { advance } from "../../core/test/lib/clock"
+import { it } from "../../core/test/lib/effect"
+import { createEmbeddedRoutes } from "../src/routes"
+import { startServer } from "./fixture/server"
+
+it.live("waits for plugin initialization before listing skills", () =>
+  Effect.gen(function* () {
+    const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-skill-endpoint-")))
+    const directory = path.join(tmp.path, "skills", "cold-skill")
+    yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
+    yield* Effect.promise(() =>
+      fs.writeFile(
+        path.join(directory, "SKILL.md"),
+        "---\nname: cold-skill\ndescription: Fixture for cold API discovery.\n---\nFixture skill.\n",
+      ),
+    )
+    yield* Effect.promise(() =>
+      fs.writeFile(path.join(tmp.path, "opencode.json"), JSON.stringify({ skills: [path.join(tmp.path, "skills")] })),
+    )
+    const server = yield* startServer(tmp.path)
+    const url = new URL("/api/skill", server.base)
+    url.searchParams.set("location[directory]", tmp.path)
+    const response = yield* Effect.promise(() => fetch(url, { headers: server.headers }))
+
+    expect(response.status).toBe(200)
+    const body: unknown = yield* Effect.promise(() => response.json())
+    if (!isRecord(body) || !Array.isArray(body["data"])) throw new Error("Expected a skill list response")
+    expect(body["data"].some((skill) => isRecord(skill) && skill["name"] === "cold-skill")).toBeTrue()
+
+    const warm = yield* Effect.promise(() => fetch(url, { headers: server.headers }))
+    expect(warm.status).toBe(200)
+    const settled: unknown = yield* Effect.promise(() => warm.json())
+    if (!isRecord(settled) || !Array.isArray(settled["data"])) throw new Error("Expected a warm skill list response")
+    expect(settled["data"].some((skill) => isRecord(skill) && skill["name"] === "cold-skill")).toBeTrue()
+  }),
+)
+
+it.live("does not expose partial skills and preserves a settled empty registry", () =>
+  Effect.gen(function* () {
+    const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-skill-gate-")))
+    const server = yield* embedded(tmp.path)
+    const entered = yield* Deferred.make<void>()
+    const gate = yield* Deferred.make<void>()
+    const partial = {
+      id: Skill.ID.make("partial"),
+      name: Skill.Name.make("partial"),
+      path: AbsolutePath.make(path.join(tmp.path, "partial.md")),
+      content: "Partial skill.",
+    }
+    yield* server.sdk.register(
+      define({
+        id: "gated-skills",
+        effect: (ctx) =>
+          Effect.gen(function* () {
+            yield* ctx.skill.transform((editor) => editor.add(partial))
+            yield* Deferred.succeed(entered, undefined)
+            yield* Deferred.await(gate)
+            yield* ctx.skill.transform((editor) =>
+              editor.add({ ...partial, id: Skill.ID.make("complete"), name: Skill.Name.make("complete") }),
+            )
+          }),
+      }),
+    )
+    const pending = yield* server.request().pipe(Effect.forkScoped({ startImmediately: true }))
+    yield* Deferred.await(entered)
+    yield* Deferred.succeed(gate, undefined)
+    const response = yield* Fiber.join(pending)
+    expect(response.status).toBe(200)
+    const body: unknown = yield* Effect.promise(() => response.json())
+    if (!isRecord(body) || !Array.isArray(body["data"])) throw new Error("Expected a settled skill list")
+    expect(body["data"].some((skill) => isRecord(skill) && skill["name"] === "complete")).toBeTrue()
+
+    yield* Skill.Service.use((skills) =>
+      skills.transform((editor) => {
+        for (const skill of editor.list()) editor.remove(skill.id)
+      }),
+    ).pipe(Effect.provide(server.locations.get(Location.Ref.make({ directory: AbsolutePath.make(tmp.path) }))))
+    const empty = yield* server.request()
+    expect(empty.status).toBe(200)
+    expect(yield* Effect.promise(() => empty.json())).toMatchObject({ data: [] })
+  }),
+)
+
+it.effect("returns a typed 503 when skill plugin activation does not settle", () =>
+  Effect.gen(function* () {
+    const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-skill-timeout-")))
+    const server = yield* embedded(tmp.path)
+    const entered = yield* Deferred.make<void>()
+    yield* server.sdk.register(
+      define({
+        id: "stalled-skills",
+        effect: () => Deferred.succeed(entered, undefined).pipe(Effect.andThen(Effect.never)),
+      }),
+    )
+    const pending = yield* server.request().pipe(Effect.forkScoped({ startImmediately: true }))
+    yield* Deferred.await(entered)
+    yield* advance(() => pending.pollUnsafe() !== undefined)
+    const response = yield* Fiber.join(pending)
+    expect(response.status).toBe(503)
+    expect(yield* Effect.promise(() => response.json())).toMatchObject({
+      _tag: "ServiceUnavailableError",
+      service: "plugin",
+    })
+  }),
+)
+
+const embedded = Effect.fnUntraced(function* (directory: string) {
+  const parent = yield* Effect.context()
+  const context = Context.merge(
+    parent,
+    yield* Layer.build(
+      createEmbeddedRoutes(
+        {
+          database: { path: ":memory:" },
+          config: { project: false, claude: false },
+          fs: { filewatcher: false },
+          models: { fetch: false },
+        },
+        [Global.node.replace(tempGlobalLayer)],
+      ).pipe(Layer.provide(HttpServer.layerServices)),
+    ),
+  )
+  const handler = Context.get(context, HttpRouter.HttpRouter).asHttpEffect().pipe(HttpEffect.toWebHandlerWith(context))
+  return {
+    sdk: Context.get(context, SdkPlugins.Service),
+    locations: Context.get(context, LocationServiceMap.Service),
+    request: () =>
+      Effect.promise(() =>
+        handler(new Request(`http://opencode.local/api/skill?location[directory]=${encodeURIComponent(directory)}`)),
+      ),
+  }
+})
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === "object" && value !== null && !Array.isArray(value)
+}
diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts
--- a/packages/client/src/promise/generated/client.ts
+++ b/packages/client/src/promise/generated/client.ts
@@ -1767,7 +1767,7 @@
             path: `/api/skill`,
             query: { location: input?.["location"] },
             successStatus: 200,
-            declaredStatuses: [400, 401, 404],
+            declaredStatuses: [400, 401, 404, 503],
             empty: false,
           },
           requestOptions,
diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json
--- a/packages/protocol/openapi.json
+++ b/packages/protocol/openapi.json
@@ -7781,6 +7781,247 @@
         "summary": "List MCP resources"
       }
     },
+    "/api/experimental/client-mcp/token": {
+      "post": {
+        "tags": ["mcp"],
+        "operationId": "experimental.mcp.client.token",
+        "parameters": [
+          {
+            "name": "x-opencode-ticket",
+            "in": "header",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false
+          },
+          {
+            "name": "location",
+            "in": "query",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "object",
+                  "properties": {
+                    "directory": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    }
+                  },
+                  "additionalProperties": false
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false,
+            "style": "deepObject",
+            "explode": true
+          },
+          {
+            "name": "name",
+            "in": "query",
+            "schema": {
+              "type": "string"
+            },
+            "required": true
+          }
+        ],
+        "security": [],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "location": {
+                      "$ref": "#/components/schemas/Location.PublicRef"
+                    },
+                    "data": {
+                      "$ref": "#/components/schemas/PtyTicket.ConnectToken"
+                    }
+                  },
+                  "required": ["location", "data"],
+                  "additionalProperties": false
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "InvalidRequestError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidRequestErrorEncoded"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "UnauthorizedError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UnauthorizedErrorEncoded"
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "ForbiddenError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ForbiddenErrorEncoded"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "LocationNotFoundError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/LocationNotFoundErrorEncoded"
+                }
+              }
+            }
+          }
+        },
+        "description": "Create a short-lived single-use ticket for opening a client MCP WebSocket connection from a browser.",
+        "summary": "Create client MCP WebSocket token"
+      }
+    },
+    "/api/experimental/client-mcp": {
+      "get": {
+        "tags": ["mcp"],
+        "operationId": "experimental.mcp.client",
+        "parameters": [
+          {
+            "name": "location",
+            "in": "query",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "object",
+                  "properties": {
+                    "directory": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    }
+                  },
+                  "additionalProperties": false
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false
+          },
+          {
+            "name": "name",
+            "in": "query",
+            "schema": {
+              "type": "string"
+            },
+            "required": true
+          },
+          {
+            "name": "ticket",
+            "in": "query",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false
+          }
+        ],
+        "security": [],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "boolean"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "InvalidRequestError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidRequestErrorEncoded"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "UnauthorizedError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UnauthorizedErrorEncoded"
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "ForbiddenError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ForbiddenErrorEncoded"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "LocationNotFoundError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/LocationNotFoundErrorEncoded"
+                }
+              }
+            }
+          }
+        },
+        "description": "Upgrade to a WebSocket over which the client hosts an MCP server named after `name`; its tools stay available while the socket is open. One JSON-RPC message per text frame. Experimental: may change without compatibility guarantees.",
+        "summary": "Attach client MCP server",
+        "x-websocket": true
+      }
+    },
     "/api/credential": {
       "get": {
         "tags": ["credential"],
@@ -9687,6 +9928,16 @@
               "application/json": {
                 "schema": {
                   "$ref": "#/components/schemas/LocationNotFoundErrorEncoded"
+                }
+              }
+            }
+          },
+          "503": {
+            "description": "ServiceUnavailableError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ServiceUnavailableErrorEncoded"
                 }
               }
             }

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server: skill.list is empty on startup

2 participants