From 7971cf1a09e3b87febd5de8de1d0f5a730e7764d Mon Sep 17 00:00:00 2001 From: lesetong Date: Mon, 10 Aug 2026 00:45:54 +0800 Subject: [PATCH] fix: emit #/$defs/ refs in set_tasks schema TypeBox Type.Cyclic generates bare local refs ("AddTask") which strict moonshot-flavored JSON Schema gateways (Console Go upstream) reject with HTTP 400. Rewrite all local $ref values to the #/$defs/ form after building the schema, and add a regression test asserting the ref format. Bump version to 0.2.2. --- package.json | 2 +- src/tools.ts | 47 ++++++++++++++++++++++++++++++++++----------- tests/tools.test.ts | 11 +++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 9380e95..140b0b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pi-todo-herdr", - "version": "0.2.1", + "version": "0.2.2", "description": "Hierarchical task tools for Pi with a live widget and Herdr sidebar integration.", "type": "module", "main": "./index.ts", diff --git a/src/tools.ts b/src/tools.ts index dd08f8b..5d0d5b1 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -15,6 +15,29 @@ const TaskStatusSchema = StringEnum(["pending", "in-progress", "blocked", "done" const SetTaskModeSchema = StringEnum(["append", "replace"] as const); const RemovalReasonSchema = StringEnum(["mistaken", "duplicate", "canceled"] as const); +/** + * TypeBox's Type.Cyclic emits bare local refs ("AddTask") instead of URI + * references ("#/$defs/AddTask"). Strict JSON Schema gateways (e.g. the + * moonshot-flavored validator behind Console Go) reject those with a 400. + * Rewrite every local $ref in place to the #/$defs/ form. + */ +function normalizeLocalRefs(schema: T): T { + if (Array.isArray(schema)) { + for (const item of schema) normalizeLocalRefs(item); + return schema; + } + if (schema && typeof schema === "object") { + for (const [key, value] of Object.entries(schema)) { + if (key === "$ref" && typeof value === "string" && !value.startsWith("#") && !value.includes("://")) { + (schema as Record)[key] = `#/$defs/${value}`; + } else { + normalizeLocalRefs(value); + } + } + } + return schema; +} + const AddTaskSchema = Type.Cyclic( { AddTask: Type.Object( @@ -38,17 +61,19 @@ const AddTaskSchema = Type.Cyclic( "AddTask", ); -export const SetTaskParams = Type.Object( - { - mode: SetTaskModeSchema, - tasks: Type.Array(AddTaskSchema, { - description: "Tasks to append or install. May be empty only in replace mode.", - }), - force: Type.Optional( - Type.Boolean({ description: "Use only when the user explicitly abandons unfinished tasks." }), - ), - }, - { additionalProperties: false }, +export const SetTaskParams = normalizeLocalRefs( + Type.Object( + { + mode: SetTaskModeSchema, + tasks: Type.Array(AddTaskSchema, { + description: "Tasks to append or install. May be empty only in replace mode.", + }), + force: Type.Optional( + Type.Boolean({ description: "Use only when the user explicitly abandons unfinished tasks." }), + ), + }, + { additionalProperties: false }, + ), ); const TaskPatchSchema = Type.Object( diff --git a/tests/tools.test.ts b/tests/tools.test.ts index 9c5a9c8..33d1d49 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -31,6 +31,17 @@ test("registers five focused tools without the old add_task alias", () => { assert.deepEqual(Object.keys(tools.get("get_task").parameters.properties), ["ids"]); }); +test("set_tasks schema uses #/$defs/ refs accepted by strict providers", () => { + const { tools } = registeredTools(); + const itemSchema = tools.get("set_tasks").parameters.properties.tasks.items; + // Every local $ref must be a URI reference; bare names ("AddTask") are + // rejected by moonshot-flavored schema validators (Console Go upstream). + assert.equal(itemSchema.$ref, "#/$defs/AddTask"); + assert.equal(itemSchema.$defs.AddTask.properties.children.items.$ref, "#/$defs/AddTask"); + const serialized = JSON.stringify(tools.get("set_tasks").parameters); + assert.ok(!/"\$ref":"(?!\#)/.test(serialized), "no bare $ref values remain"); +}); + test("set_tasks appends optional-description tasks and persists a complete snapshot", async () => { const harness = registeredTools(); const result = await execute(harness.tools.get("set_tasks"), {