diff --git a/extensions/codex/src/app-server/dynamic-tool-build.test.ts b/extensions/codex/src/app-server/dynamic-tool-build.test.ts index 46a720205f0a..609f1bfb767c 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.test.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.test.ts @@ -442,6 +442,25 @@ describe("Codex app-server dynamic tool build", () => { }); }); + it("forwards the task-suggestion delivery mode", async () => { + // Regression: spawn_task/dismiss_task silently never existed on the Codex + // app-server path because this harness dropped params.taskSuggestionDeliveryMode. + const workspaceDir = path.join(tempDir, "workspace"); + const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); + params.disableTools = false; + params.runtimePlan = createCodexRuntimePlanFixture(); + params.taskSuggestionDeliveryMode = "gateway"; + let receivedOptions: unknown; + setOpenClawCodingToolsFactoryForTests((options) => { + receivedOptions = options; + return [createRuntimeDynamicTool("message")]; + }); + + await buildDynamicToolsForTest(params, workspaceDir); + + expect(receivedOptions).toMatchObject({ taskSuggestionDeliveryMode: "gateway" }); + }); + it("preserves the host-provided OpenClaw tool through the Codex allowlist", async () => { const workspaceDir = path.join(tempDir, "workspace"); const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); diff --git a/extensions/codex/src/app-server/dynamic-tool-build.ts b/extensions/codex/src/app-server/dynamic-tool-build.ts index 26fdd77af5bc..ce784a5fe108 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.ts @@ -328,6 +328,9 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) { requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey), sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, + // Same sibling-harness rule as clientCaps above: without this forward, + // spawn_task/dismiss_task silently never exist for Codex-harness runs. + taskSuggestionDeliveryMode: params.taskSuggestionDeliveryMode, disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool, forceMessageTool: shouldForceMessageTool(messagePolicyParams), enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true, diff --git a/src/agents/tools/task-suggestion-tools.ts b/src/agents/tools/task-suggestion-tools.ts index 571e13be6a70..b2b496e7a619 100644 --- a/src/agents/tools/task-suggestion-tools.ts +++ b/src/agents/tools/task-suggestion-tools.ts @@ -17,17 +17,20 @@ const SpawnTaskToolSchema = Type.Object( title: Type.String({ minLength: 1, maxLength: 60, - description: "Imperative task title under 60 characters.", + description: + "Imperative task title under 60 characters (start with a verb); shown as the card title and the started session's name.", }), prompt: Type.String({ minLength: 1, maxLength: 32_768, - description: "Self-contained task prompt with relevant file paths and context.", + description: + "Self-contained task prompt with file paths and enough context to act without this conversation.", }), tldr: Type.String({ minLength: 1, maxLength: 1_024, - description: "One or two plain-language sentences explaining the value; no code or paths.", + description: + "One or two plain-language sentences shown on the card explaining the value; no code or paths.", }), cwd: Type.Optional( Type.String({ @@ -74,8 +77,14 @@ export function createTaskSuggestionTools(params: { name: "spawn_task", displaySummary: SPAWN_TASK_TOOL_DISPLAY_SUMMARY, description: [ - "Suggest confirmed valuable out-of-scope follow-up: dead code, stale docs, missing coverage, verified TODO, security issue.", - "Operator suggestion only; does not start work. cwd must be an absolute path inside a git checkout.", + "Flag an out-of-scope issue as a separate follow-up task instead of ignoring it, fixing it inline, or only mentioning it in your reply — a follow-up described in prose is lost; recording it here is what surfaces it to the operator.", + "This is the tool behind requests like 'flag it as a follow-up', 'note that for later', or 'make a task for that'; whenever you would write 'Follow-up:' in a reply, call this instead.", + "Use this whenever work you were not asked to do surfaces along the way: dead code, stale docs, missing coverage, a confirmed TODO, or a security issue spotted in passing.", + "Requests to stay scoped or skip cleanup apply to doing the work, not to flagging it: this only records a suggestion card in the operator's UI; nothing runs unless they accept it, and your current turn continues uninterrupted.", + "Do not flag vague code-smell observations or low-confidence hunches.", + "The prompt must stand alone: the started task sees only that text, never this conversation.", + "cwd must be an absolute path inside a git checkout.", + "Suggestions are ephemeral; ids do not survive a gateway restart.", ].join(" "), parameters: SpawnTaskToolSchema, outputSchema: SpawnTaskOutputSchema, @@ -110,8 +119,11 @@ export function createTaskSuggestionTools(params: { label: "Dismiss Task", name: "dismiss_task", displaySummary: DISMISS_TASK_TOOL_DISPLAY_SUMMARY, - description: - "Withdraw stale/irrelevant pending spawn_task. Accepted suggestion cannot withdraw.", + description: [ + "Withdraw a pending suggestion card you created with spawn_task when it is now stale, superseded, or already handled in this session.", + "To replace a card, call spawn_task with the better suggestion first, then dismiss the old task_id.", + "Only cards the operator has not acted on can be withdrawn; accepted ones cannot.", + ].join(" "), parameters: DismissTaskToolSchema, execute: async (_toolCallId, args) => { const input = args as Record;