From b88c9e235b19f41922b225a5758fa190e016e724 Mon Sep 17 00:00:00 2001 From: Abe Isleem <29489334+abeisleem@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:14:16 +0200 Subject: [PATCH 1/2] fix: convert v2 goal plan input to json schema --- dist/server.js | 2 +- src/server.ts | 4 +++- test/server-v2.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/dist/server.js b/dist/server.js index 2123b6a..243983a 100644 --- a/dist/server.js +++ b/dist/server.js @@ -4948,7 +4948,7 @@ function goalToolsV2(services) { { name: "update_goal_plan", description: services.locale === "zh-CN" ? "\u4FDD\u5B58\u76EE\u6807\u7684\u6574\u4F53\u8BA1\u5212\u3001\u9636\u6BB5\u3001\u4EFB\u52A1\u548C\u9A8C\u8BC1\u8BC1\u636E\u3002\u4FDD\u6301\u6574\u4F53\u76EE\u6807\u4E0D\u53D8\uFF1B\u4F7F\u7528 get_goal \u8FD4\u56DE\u7684\u76EE\u6807 ID \u548C\u8BA1\u5212\u7248\u672C\u3002" : "Persist the overall plan, phases, tasks, verification evidence and decisions. Preserve the goal scope; use the goal ID and planRevision from get_goal. Completed work cannot be silently reopened or removed.", - input: v2ObjectSchema(planToolArgs), + input: z2.toJSONSchema(PlanToolSchema, { io: "input", target: "draft-7" }), options: { codemode: false }, execute: async (args, context) => ({ content: await planFromTool(args, context) }) }, diff --git a/src/server.ts b/src/server.ts index c0ac50e..36f3fd5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3356,7 +3356,9 @@ function goalToolsV2(services: GoalServices): ToolV2Info[] { { name: "update_goal_plan", description: services.locale === "zh-CN" ? "保存目标的整体计划、阶段、任务和验证证据。保持整体目标不变;使用 get_goal 返回的目标 ID 和计划版本。" : "Persist the overall plan, phases, tasks, verification evidence and decisions. Preserve the goal scope; use the goal ID and planRevision from get_goal. Completed work cannot be silently reopened or removed.", - input: v2ObjectSchema(planToolArgs), + // V2 accepts JSON Schema, not the Zod objects used by the V1 tool API. + // Input mode preserves optional evidence and defaulted plan decisions. + input: z.toJSONSchema(PlanToolSchema, { io: "input", target: "draft-7" }) as ToolSchema.ValueSchema, options: { codemode: false }, execute: async (args, context) => ({ content: await planFromTool(args, context) }), }, diff --git a/test/server-v2.test.ts b/test/server-v2.test.ts index b71f018..7975e1e 100644 --- a/test/server-v2.test.ts +++ b/test/server-v2.test.ts @@ -339,6 +339,45 @@ test("V2 setup registers goal tools with JSON Schema inputs, codemode:false, and expect(mock.promptCalls).toHaveLength(0) }) +test("V2 plan tool publishes JSON Schema with required fields and optional nested evidence", async () => { + const mock = makeMockContext({ auto_continue: false }) + await setupPlugin(mock as never) + // Inspect the serialized wire schema, not only the outer object wrapper. + const input = JSON.parse(JSON.stringify(goalTool(mock, "update_goal_plan").input)) + expect(input.required).toEqual(["goal_id", "expected_revision", "plan", "reason"]) + expect(input.additionalProperties).toBe(false) + expect(input.properties.goal_id).toMatchObject({ type: "string", minLength: 1 }) + expect(input.properties.expected_revision).toMatchObject({ type: "integer", minimum: 0 }) + expect(input.properties.reason).toMatchObject({ type: "string", minLength: 1, maxLength: 2000 }) + expect(input.properties.revisit_evidence).toMatchObject({ type: "string", minLength: 1, maxLength: 2000 }) + + const plan = input.properties.plan + expect(plan.required).toEqual(["summary", "completionCriteria", "phases"]) + expect(plan.additionalProperties).toBe(false) + expect(plan.properties.decisions).toMatchObject({ type: "array", default: [], maxItems: 32 }) + expect(plan.properties.completionCriteria).toMatchObject({ type: "array", minItems: 1, maxItems: 32 }) + const phases = plan.properties.phases + expect(phases).toMatchObject({ type: "array", minItems: 1, maxItems: 64 }) + const phase = phases.items + expect(phase.required).toEqual(["id", "objective", "status", "tasks"]) + expect(phase.additionalProperties).toBe(false) + const tasks = phase.properties.tasks + expect(tasks).toMatchObject({ type: "array", minItems: 1, maxItems: 128 }) + expect(tasks.items.required).toEqual(["id", "description", "status"]) + expect(tasks.items.additionalProperties).toBe(false) + expect(tasks.items.properties.status).toEqual({ + type: "string", + enum: ["pending", "in_progress", "completed", "blocked"], + }) + for (const field of [phase.properties.verification, phase.properties.blocker, + tasks.items.properties.evidence, tasks.items.properties.blocker]) { + expect(field.anyOf).toEqual([ + { type: "string", minLength: 1, maxLength: 2000 }, + { type: "null" }, + ]) + } +}) + test("V2 stop, replace, clear, and history tools preserve prior goals", async () => { const mock = makeMockContext({ auto_continue: false }) const cleanup = await setupPlugin(mock as never) From e44a291e5d5aeeadfe60df8894fec49511793854 Mon Sep 17 00:00:00 2001 From: Abe Isleem <29489334+abeisleem@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:31:12 +0200 Subject: [PATCH 2/2] test: clarify nested plan schema assertions --- test/server-v2.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/server-v2.test.ts b/test/server-v2.test.ts index 7975e1e..c8074d1 100644 --- a/test/server-v2.test.ts +++ b/test/server-v2.test.ts @@ -363,14 +363,19 @@ test("V2 plan tool publishes JSON Schema with required fields and optional neste expect(phase.additionalProperties).toBe(false) const tasks = phase.properties.tasks expect(tasks).toMatchObject({ type: "array", minItems: 1, maxItems: 128 }) - expect(tasks.items.required).toEqual(["id", "description", "status"]) - expect(tasks.items.additionalProperties).toBe(false) - expect(tasks.items.properties.status).toEqual({ + const task = tasks.items + expect(task.required).toEqual(["id", "description", "status"]) + expect(task.additionalProperties).toBe(false) + expect(task.properties.status).toEqual({ type: "string", enum: ["pending", "in_progress", "completed", "blocked"], }) - for (const field of [phase.properties.verification, phase.properties.blocker, - tasks.items.properties.evidence, tasks.items.properties.blocker]) { + for (const field of [ + phase.properties.verification, + phase.properties.blocker, + task.properties.evidence, + task.properties.blocker, + ]) { expect(field.anyOf).toEqual([ { type: "string", minLength: 1, maxLength: 2000 }, { type: "null" },