From aefae5178c709da96f986a5be8e8778b889c78e5 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Wed, 3 Jun 2026 08:37:20 +0000 Subject: [PATCH] fix(mcp): make axme_finalize_close required-field errors agent-actionable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The schema for axme_finalize_close declares 6 handoff strings as required z.string() (stopped_at, summary, in_progress, next_steps, worklog_entry, startup_text). When an agent omits any of them Zod emits "Expected string, received undefined" per missing field — which has been mis-read as a per-field server bug rather than a missing argument. Two changes: - Add .min(1, "") to all six fields. The message names the field, marks it REQUIRED, and gives an empty-state placeholder ("(nothing in progress)" / "(none — work is complete)") so the agent knows what to pass when there's literally nothing to report. No behavior change for valid (non-empty-string) calls. - Mark each field as [REQUIRED] / [optional] in the .describe() text and in the axme_begin_close checklist output. Required block now carries an explicit "ALL must be present and non-empty" warning with omit-is-error rationale. 608/608 tests pass; type-check clean; bundle builds. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server.ts | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/server.ts b/src/server.ts index 8768e3c..efe4f80 100644 --- a/src/server.ts +++ b/src/server.ts @@ -883,7 +883,7 @@ server.tool( "", "## Step 2: Prepare Everything for `axme_finalize_close`", "", - "Collect ALL data into a single `axme_finalize_close` call:", + "Collect ALL data into a single `axme_finalize_close` call.", "", "### Extractions (arrays, can be empty):", "- `memories`: [{action, type, title, description, body, keywords, scope}]", @@ -893,15 +893,19 @@ server.tool( "- `safety_rules`: [{action, rule_type, value}]", " - action: `add` | `remove`", "", - "### Handoff:", - "- `stopped_at`: what the session stopped at (single line)", - "- `summary`: 2-5 bullet points of what was accomplished", - "- `in_progress`: current state (branches, PRs, uncommitted work)", + "### Handoff — six REQUIRED string fields. ALL must be present and non-empty.", + "If a session legitimately has nothing to report for one of them, pass an explicit placeholder string (examples below) — do NOT omit the field. Omitting yields per-field 'Expected string, received undefined' errors from Zod.", + "", + "- **`stopped_at`** (REQUIRED, single line) — where the session stopped, e.g. `\"finalized PR #207 review\"`", + "- **`summary`** (REQUIRED) — 2-5 bullet points of accomplishments. Empty placeholder: `\"- (no items)\"`", + "- **`in_progress`** (REQUIRED) — branches, PRs, uncommitted work. Empty placeholder: `\"(nothing in progress)\"`", + "- **`next_steps`** (REQUIRED) — concrete next steps. Empty placeholder: `\"(none — work is complete)\"`", + "- **`worklog_entry`** (REQUIRED) — 5-15 line narrative markdown summary of the session", + "- **`startup_text`** (REQUIRED) — ready-to-paste startup text for the next session", + "", + "### Handoff — optional fields (omit if not applicable):", "- `prs`: [{url, title, status}]", - "- `test_results`, `blockers`, `dirty_branches` (optional)", - "- `next_steps`: concrete next steps", - "- `worklog_entry`: narrative summary (5-15 lines markdown)", - "- `startup_text`: ready-to-paste text for next session", + "- `test_results`, `blockers`, `dirty_branches`", "", "## Step 3: Call `axme_finalize_close`", "", @@ -946,20 +950,26 @@ server.tool( value: z.string(), })).optional().describe("Safety rules to add/remove"), // --- Handoff --- - stopped_at: z.string().describe("What the session stopped at (single line)"), - summary: z.string().describe("2-5 bullet points of what was accomplished. Use real newlines, NOT literal backslash-n. Each bullet on its own line starting with '- '."), - in_progress: z.string().describe("Current state: branches, PRs, uncommitted work. Use real newlines, NOT literal backslash-n."), + // All six strings below are REQUIRED. If a session legitimately has + // nothing to report for one, pass an explicit placeholder like + // "(nothing in progress)" — do NOT omit the field. Omitting yields a + // Zod "Expected string, received undefined" error per missing field, + // which has historically been mis-read by agents as a per-field server + // bug rather than a missing-argument error. + stopped_at: z.string().min(1, "stopped_at is REQUIRED — pass a single-line description of where the session stopped, e.g. 'finalized PR #207 review'.").describe("[REQUIRED] What the session stopped at (single line)"), + summary: z.string().min(1, "summary is REQUIRED — pass 2-5 bullet points of accomplishments separated by real newlines, or '- (no items)' if truly empty.").describe("[REQUIRED] 2-5 bullet points of what was accomplished. Use real newlines, NOT literal backslash-n. Each bullet on its own line starting with '- '."), + in_progress: z.string().min(1, "in_progress is REQUIRED — pass branches / PRs / uncommitted work, or '(nothing in progress)' if the working tree is clean. Do not omit the field.").describe("[REQUIRED] Current state: branches, PRs, uncommitted work. Use real newlines, NOT literal backslash-n. Pass '(nothing in progress)' if clean."), prs: z.array(z.object({ url: z.string(), title: z.string(), status: z.string(), - })).optional().describe("PRs created/merged in this session"), - test_results: z.string().optional().describe("Test run summary"), - blockers: z.string().optional().describe("Blockers for next session"), - next_steps: z.string().describe("Concrete next steps for next session. Use real newlines, NOT literal backslash-n."), - dirty_branches: z.string().optional().describe("Branch names with state"), - worklog_entry: z.string().describe("Narrative session summary (5-15 lines markdown). Use real newlines, NOT literal backslash-n."), - startup_text: z.string().describe("Ready-to-paste startup text for the next session"), + })).optional().describe("[optional] PRs created/merged in this session"), + test_results: z.string().optional().describe("[optional] Test run summary"), + blockers: z.string().optional().describe("[optional] Blockers for next session"), + next_steps: z.string().min(1, "next_steps is REQUIRED — pass concrete next steps for the next session, or '(none — work is complete)' if there are none. Do not omit the field.").describe("[REQUIRED] Concrete next steps for next session. Use real newlines, NOT literal backslash-n. Pass '(none — work is complete)' if there are none."), + dirty_branches: z.string().optional().describe("[optional] Branch names with state"), + worklog_entry: z.string().min(1, "worklog_entry is REQUIRED — pass a 5-15 line narrative summary of the session in markdown. Do not omit the field.").describe("[REQUIRED] Narrative session summary (5-15 lines markdown). Use real newlines, NOT literal backslash-n."), + startup_text: z.string().min(1, "startup_text is REQUIRED — pass ready-to-paste text the user will hand to the next session. Do not omit the field.").describe("[REQUIRED] Ready-to-paste startup text for the next session"), }, async (args) => { const sid = getOwnedSessionIdForLogging();