From bee7013db0fb805e9bdb3897a4714ed5be065032 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 06:51:27 +0000 Subject: [PATCH 1/2] test(server): a Claude /goal that mentions a skill stays one goal command Reproduces #29750: makeClaudeUserMessage splits '/goal ... $skill' into two text blocks, so the Claude CLI never runs /goal and no goal is set. The test fails on main; there is no fix yet. AI-assisted; verified with the tests and tools listed above. --- .../Adapters/ClaudeAdapterV2.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 82004eee5e035..2641b058d8472 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -5058,6 +5058,20 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + // regression: #29750 Claude sets a goal only from a prompt that opens with /goal + it("keeps a /goal that mentions a skill as one native goal command", () => { + const message = ClaudeAdapterV2.makeClaudeUserMessage({ + text: "/goal fix all the gaps.\ndecompile target.ipa into ./target/\n$review", + skills: new Map([["review", { path: "/home/me/.claude/skills/review/SKILL.md" }]]), + }); + const content = message.message.content; + assert.isString(content); + assert.match( + content as string, + /^\/goal fix all the gaps\.\ndecompile target\.ipa into \.\/target\/\n/, + ); + }); + // regression: #15708 Stop must not silently drop a steer Claude queued behind a foreground task it.effect.each([ { From b521e8c3ce0013448215b6ea933d252980e4b346 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 06:56:35 +0000 Subject: [PATCH 2/2] fix(server): Claude sets a /goal that also mentions a skill makeClaudeUserMessage split a prompt mentioning a known skill into a leading block plus a trailing /skill block. Claude only runs /goal when it opens the prompt, so '/goal ... $skill' never set a goal while the server still treated the message as a goal command. Send a goal command as one string, with skill mentions rewritten inline as /name. Fixes pingdotgg/t3code#29750 AI-assisted; verified with the tests and tools listed above. --- .../Adapters/ClaudeAdapterV2.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 19e77d4c98eee..0f15e77c94b5a 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -97,6 +97,7 @@ import { TrimmedNonEmptyString, defaultInstanceIdForDriver, isCompactionCommandMessage, + parseGoalCommand, type MessageId, type ModelSelection, type OrchestrationV2ConversationMessage, @@ -2551,18 +2552,22 @@ export function makeClaudeUserMessage(input: { // Keep the whole request before that block; see ClaudeSkillDispatch. const dispatch = input.skills === undefined ? undefined : planClaudeSkillDispatch(input.text, input.skills); + // Claude runs `/goal` only when it opens the prompt, so a goal stays one block. + const isGoalCommand = parseGoalCommand({ text: input.text }) !== null; return { type: "user", message: { role: "user", - content: dispatch - ? [ - ...(dispatch.leadingText === undefined - ? [] - : [{ type: "text" as const, text: dispatch.leadingText }]), - { type: "text" as const, text: dispatch.commandText }, - ] - : input.text, + content: isGoalCommand + ? (dispatch?.leadingText ?? input.text) + : dispatch + ? [ + ...(dispatch.leadingText === undefined + ? [] + : [{ type: "text" as const, text: dispatch.leadingText }]), + { type: "text" as const, text: dispatch.commandText }, + ] + : input.text, }, parent_tool_use_id: null, ...(input.priority === undefined ? {} : { priority: input.priority }),