From acb4b3a3f7ce311b3a9f79c401ee1c6fbe61511a Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Fri, 31 Jul 2026 19:39:32 -0500 Subject: [PATCH 1/3] Make MCP development task creation reliable --- docs/MCP_SERVER.md | 51 ++++- .../web/src/lib/__tests__/mcp-server.test.ts | 175 +++++++++++++++++ .../lib/__tests__/mcp-tool-catalog.test.ts | 4 + .../src/lib/__tests__/tasks.server.test.ts | 94 +++++++++- packages/web/src/lib/mcp-instructions.ts | 4 +- packages/web/src/lib/mcp-server.ts | 59 ++++-- packages/web/src/lib/tasks.server.ts | 176 +++++++++++------- 7 files changed, 463 insertions(+), 100 deletions(-) diff --git a/docs/MCP_SERVER.md b/docs/MCP_SERVER.md index 8a910aa01..1a7d58f56 100644 --- a/docs/MCP_SERVER.md +++ b/docs/MCP_SERVER.md @@ -99,17 +99,21 @@ Do not request `tasks:admin` for personal planning. Public manual search and pub Task-listing tools take `visibility: "all" | "public" | "private"` — signed-in callers default to `all` (public plus their own private work). On the tools that previously exposed `scope`/`taskScope`, `"accessible"` survives as a deprecated alias for `"all"`; `listTasks` never had the alias and takes only `visibility`. -Example private task: +Example private task (use the `personalRoot.id` returned by `getMe`): ```json { "title": "File federal taxes", + "description": "Prepare, review, and file the federal and state returns before the legal deadline.", + "parentTaskId": "", + "taskKey": "personal:taxes:2026", + "category": "OTHER", "hours": 3, "value": 20000, "p_success": 0.99, "cash_cost": 150, - "expected_deliverable": "Accepted federal and state returns with filing receipts", - "acceptance_criteria": [ + "impactStatement": "Filing correctly and on time avoids penalties, interest, and account disruption.", + "acceptanceCriteria": [ "Return totals match reviewed source documents", "Filing receipt is attached" ], @@ -120,6 +124,45 @@ Example private task: } ``` +## Creating Optimitron Development Tasks + +Improvements to Optimitron itself belong under the managed development root +whose stable task key is `optimitron:dev`. Do not attach them directly to +Optimize Earth and do not rely on a memorized database ID: + +1. Call `getMe` and confirm the connected identity has access to the + development tree. +2. Call `searchTasks` with `query: "optimitron:dev"` and + `visibility: "all"`. +3. Select the exact `taskKey: "optimitron:dev"` result. Search the proposed + title and its distinctive terms to avoid duplicating existing work. +4. Call `createTask` with `parentTaskKey: "optimitron:dev"`; use an + `optimitron:dev:` key for the new task. + +If the exact development root is not returned, stop and report the access or +search problem. Do not substitute the Optimize Earth root or a merely similar +task. + +```json +{ + "title": "Make development-parent discovery reliable in MCP", + "description": "Let agents find the canonical development root with natural task-search queries and document the exact creation flow.", + "parentTaskKey": "optimitron:dev", + "taskKey": "optimitron:dev:mcp-development-parent-discovery", + "category": "ENGINEERING", + "hours": 4, + "value": 20000, + "p_success": 0.9, + "impactStatement": "Reliable parent discovery prevents orphaned and duplicate development work.", + "acceptanceCriteria": [ + "Natural multi-word searches return the development root", + "The MCP instructions show the exact search-then-create flow", + "Regression tests cover development-root discovery" + ], + "executor_type": "AI Agent" +} +``` + ## Estimate Standards For Agents For public Earth-level tasks, agents should use `setTaskImpact` when they know enough to estimate value. For private personal tasks, prefer `createTask` / `updateTask` with `hours`, `value`, `p_success`, and `cash_cost`. If an agent does not know enough to estimate value, it should create a clarification/decomposition task rather than inventing confidence. @@ -188,7 +231,7 @@ admin gating — is the **[MCP Tool Reference](https://optimitron.com/developers (human) and `/api/mcp/tools` (machine). When this section and those sources disagree, those sources win. -- Queue discovery: `listTasks`, `getTask`, `getBlockers`, `getQueueAudit`, `getNextAction`, `getMyQueue`, `getAIQueue`, `evaluateTaskEconomics`. +- Queue discovery: `listTasks`, `searchTasks`, `getTask`, `getBlockers`, `getQueueAudit`, `getNextAction`, `getMyQueue`, `getAIQueue`, `evaluateTaskEconomics`. - Personal task management: `createTask`, `updateTask`, `deleteTask`, `proposeTaskBundle` (multi-task drafts with duplicate review), `promoteTask` (DRAFT → ACTIVE after review). - Reviewed private import: `reviewPrivateTaskBundle`, `applyPrivateTaskBundle`, `deletePrivateSourceSelection`. - Private execution (admin/agent-gated; not exposed to ordinary third-party tokens): `startTaskExecution`, `submitTaskArtifact`, `submitTaskForVerification`, `verifyTaskExecution`, `getTaskAuditTrail`. diff --git a/packages/web/src/lib/__tests__/mcp-server.test.ts b/packages/web/src/lib/__tests__/mcp-server.test.ts index 804e8de83..3abad24f8 100644 --- a/packages/web/src/lib/__tests__/mcp-server.test.ts +++ b/packages/web/src/lib/__tests__/mcp-server.test.ts @@ -31,6 +31,7 @@ import { DOCUMENT_REVIEW_TASK_KEY_PREFIX } from "../tasks/document-review-contra const mocks = vi.hoisted(() => ({ listTasks: vi.fn(), + searchTasks: vi.fn(), getTaskDetailData: vi.fn(), claimTask: vi.fn(), completeSelfTask: vi.fn(), @@ -172,6 +173,7 @@ vi.mock("../tasks.server", () => ({ completeSelfTask: mocks.completeSelfTask, completeTaskClaim: mocks.completeTaskClaim, listTasks: mocks.listTasks, + searchTasks: mocks.searchTasks, getTaskDetailData: mocks.getTaskDetailData, })); @@ -715,6 +717,7 @@ beforeEach(() => { }), ); mocks.listTasks.mockResolvedValue([]); + mocks.searchTasks.mockResolvedValue([]); mocks.claimTask.mockResolvedValue({ id: "claim-1", status: TaskClaimStatus.CLAIMED, @@ -2196,6 +2199,52 @@ describe("MCP server tool dispatch", () => { }); }); + it("routes natural development-parent searches through the authenticated task boundary", async () => { + mocks.searchTasks.mockResolvedValue([ + { + id: "optimitron-dev", + taskKey: "optimitron:dev", + title: "Optimize Optimitron: engineering program", + }, + ]); + + const client = await setup("user-1", ALL_SCOPES); + const tools = await client.listTools(); + const searchTool = tools.tools.find( + (tool) => tool.name === "searchTasks", + ); + const createTool = tools.tools.find((tool) => tool.name === "createTask"); + expect(searchTool?.description).toContain("optimitron:dev"); + expect(createTool?.description).toContain("optimitron:dev"); + + const result = await client.callTool({ + name: "searchTasks", + arguments: { + query: "find Optimize Optimitron parent", + status: "ACTIVE", + visibility: "all", + }, + }); + + expect(result.isError).toBeFalsy(); + expect(mocks.searchTasks).toHaveBeenCalledWith( + "find Optimize Optimitron parent", + expect.objectContaining({ + clientAccessBoundary: expect.any(Object), + limit: 20, + status: "ACTIVE", + userId: "user-1", + visibility: "accessible", + }), + ); + expect(parseToolBody(result)).toEqual([ + expect.objectContaining({ + id: "optimitron-dev", + taskKey: "optimitron:dev", + }), + ]); + }); + it("does not invent private visibility when listTasks omits isPublic", async () => { mocks.listTasks.mockResolvedValue([ makeCreatedTask({ @@ -4459,6 +4508,21 @@ describe("MCP server tool dispatch", () => { expect(mocks.taskCreate).not.toHaveBeenCalled(); }); + it("rejects ambiguous parent id and key inputs", async () => { + const client = await setup("user-1", ALL_SCOPES); + const result = await client.callTool({ + name: "createTask", + arguments: makeCreateTaskArguments({ + parentTaskKey: "optimitron:dev", + }), + }); + + expect(result.isError).toBe(true); + expect(parseToolBody(result).message).toContain("provide exactly one"); + expect(mocks.taskFindFirst).not.toHaveBeenCalled(); + expect(mocks.taskCreate).not.toHaveBeenCalled(); + }); + it("rejects Optimize Earth as a direct task parent", async () => { const client = await setup("user-1", ALL_SCOPES); const result = await client.callTool({ @@ -4484,6 +4548,25 @@ describe("MCP server tool dispatch", () => { expect(mocks.taskCreate).not.toHaveBeenCalled(); }); + it("rejects the Optimize Earth stable key as a direct task parent", async () => { + mocks.taskFindFirst.mockResolvedValue(makeOptimizeEarthRoot()); + const { parentTaskId: _parentTaskId, ...argumentsWithoutParentId } = + makeCreateTaskArguments({ + parentTaskKey: "program:optimize-earth", + }); + const client = await setup("user-1", ALL_SCOPES); + const result = await client.callTool({ + name: "createTask", + arguments: argumentsWithoutParentId, + }); + + expect(result.isError).toBe(true); + expect(parseToolBody(result).message).toContain( + "Optimize Earth is reserved", + ); + expect(mocks.taskCreate).not.toHaveBeenCalled(); + }); + it("response includes missingFields[] for soft-recommended fields", async () => { mocks.getTaskDetailData.mockResolvedValue({ task: makeCreatedTask({ @@ -5526,6 +5609,98 @@ describe("MCP server tool dispatch", () => { expect(data.maxClaims).toBeNull(); }); + it("createTask resolves an exact parentTaskKey without requiring an opaque parent id", async () => { + mocks.taskFindFirst.mockImplementation( + async (args?: { where?: { taskKey?: string } }) => { + if (args?.where?.taskKey === "optimitron:dev") { + return makePlanningBranch({ + id: "optimitron-dev", + taskKey: "optimitron:dev", + }); + } + return null; + }, + ); + mocks.getTaskDetailData.mockResolvedValue({ + task: makeCreatedTask({ + id: "created-task", + parentTaskId: "optimitron-dev", + contextJson: { + executor_type: "AI Agent", + value: 100, + p_success: 0.5, + cash_cost: 0, + }, + }), + }); + mocks.computeTaskPriority.mockReturnValue(makePriority()); + const { parentTaskId: _parentTaskId, ...argumentsWithoutParentId } = + makeCreateTaskArguments({ + executor_type: "AI Agent", + parentTaskKey: "optimitron:dev", + }); + + const client = await setup("user-1", ALL_SCOPES); + const result = await client.callTool({ + name: "createTask", + arguments: argumentsWithoutParentId, + }); + + expect(result.isError).toBeFalsy(); + expect(mocks.taskFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ taskKey: "optimitron:dev" }), + }), + ); + const data = ( + mocks.taskCreate.mock.calls[0]![0] as { data: Record } + ).data; + expect(data.parentTaskId).toBe("optimitron-dev"); + expect(data).not.toHaveProperty("parentTaskKey"); + }); + + it("scopes parentTaskKey resolution to the OAuth client boundary", async () => { + mocks.taskFindFirst.mockResolvedValue(null); + const clientBoundaryWhere = { + OR: [ + { isPublic: true }, + { + isPublic: false, + ownerOrganizationId: { in: ["organization-1"] }, + }, + ], + }; + mocks.getTaskClientAccessWhere.mockReturnValue(clientBoundaryWhere); + const { parentTaskId: _parentTaskId, ...argumentsWithoutParentId } = + makeCreateTaskArguments({ + parentTaskKey: "optimitron:dev", + }); + const client = await setup("user-1", [McpScope.TASKS_ORGANIZATION], { + organizationIds: ["organization-1"], + }); + + const result = await client.callTool({ + name: "createTask", + arguments: argumentsWithoutParentId, + }); + + expect(result.isError).toBe(true); + expect(parseToolBody(result).message).toContain("was not found"); + expect(mocks.getTaskClientAccessWhere).toHaveBeenCalledWith({ + allowPersonalPrivate: false, + organizationIds: ["organization-1"], + }); + expect(mocks.taskFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + taskKey: "optimitron:dev", + AND: expect.arrayContaining([clientBoundaryWhere]), + }), + }), + ); + expect(mocks.taskCreate).not.toHaveBeenCalled(); + }); + it("rejects updateTask when a non-admin user targets a public task", async () => { mocks.getTaskDetailData.mockResolvedValue({ task: makeCreatedTask({ diff --git a/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts b/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts index bcb309a43..4972100fe 100644 --- a/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts +++ b/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts @@ -78,4 +78,8 @@ describe("MCP tool catalog", () => { expect(phantom).toEqual([]); }); + + it("tells coding agents how to target the canonical development branch", () => { + expect(MCP_SERVER_INSTRUCTIONS).toContain('parentTaskKey "optimitron:dev"'); + }); }); diff --git a/packages/web/src/lib/__tests__/tasks.server.test.ts b/packages/web/src/lib/__tests__/tasks.server.test.ts index 2da9ff3c6..502975389 100644 --- a/packages/web/src/lib/__tests__/tasks.server.test.ts +++ b/packages/web/src/lib/__tests__/tasks.server.test.ts @@ -719,24 +719,98 @@ describe("tasks server", () => { ]); }); - it("searchTasks without a user searches public tasks only", async () => { - await searchTasks("secret grant memo", { userId: null }); + it("searchTasks without a user searches public tasks using OR-ranked terms", async () => { + await searchTasks("find secret grant memo task", { userId: null }); const args = lastTaskFindManyArgs(); const filters = (args.where as { AND: unknown[] }).AND; expect(filters[0]).toEqual( expect.objectContaining({ deletedAt: null, isPublic: true }), ); - expect(filters.slice(1)).toHaveLength(3); + expect(filters.slice(1)).toHaveLength(1); + const termFilter = filters[1] as { OR: unknown[] }; for (const term of ["secret", "grant", "memo"]) { - expect(filters.slice(1)).toContainEqual( - expect.objectContaining({ - OR: expect.arrayContaining([ - { title: { contains: term, mode: "insensitive" } }, - ]), - }), - ); + expect(termFilter.OR).toContainEqual({ + title: { contains: term, mode: "insensitive" }, + }); } + expect(termFilter.OR).not.toContainEqual({ + title: { contains: "find", mode: "insensitive" }, + }); + expect(termFilter.OR).not.toContainEqual({ + title: { contains: "task", mode: "insensitive" }, + }); + expect(args.take).toBe(128); + }); + + it.each([ + ["Optimize Optimitron parent", "optimitron:dev"], + ["estimate calibration guard", "optimitron:dev:estimate-calibration-guard"], + ["duplicate detection semantic", "optimitron:dev:semantic-dedup"], + ])( + "searchTasks ranks the expected task for natural multi-word query %s", + async (query, expectedTaskKey) => { + mocks.prisma.taskFindMany.mockResolvedValue([ + mockTask({ + description: "Container for self-improvement development tasks.", + id: "optimitron-dev", + taskKey: "optimitron:dev", + title: "Optimize Optimitron: engineering program", + }), + mockTask({ + description: "Reject stale or uncalibrated task estimates.", + id: "estimate-calibration", + taskKey: "optimitron:dev:estimate-calibration-guard", + title: "Add an estimate calibration guard", + }), + mockTask({ + description: "Detect semantically similar duplicate tasks.", + id: "semantic-dedup", + taskKey: "optimitron:dev:semantic-dedup", + title: "Build semantic duplicate detection", + }), + ]); + + const results = await searchTasks(query, { userId: "user-a" }); + + expect(results[0]?.taskKey).toBe(expectedTaskKey); + }, + ); + + it("uses an exact-key fast path and ranks it ahead of stronger prefix matches", async () => { + mocks.prisma.taskFindFirst.mockResolvedValue( + mockTask({ + description: "Container for self-improvement development tasks.", + id: "optimitron-dev", + taskKey: "optimitron:dev", + title: "Optimize Optimitron: engineering program", + }), + ); + const childTasks = Array.from({ length: 25 }, (_, index) => + mockTask({ + description: + "Build optimitron:dev parent discovery for Optimitron development.", + id: `dev-child-${index}`, + taskKey: `optimitron:dev:child-${index}`, + title: `Optimitron dev development task ${index}`, + }), + ); + mocks.prisma.taskFindMany.mockResolvedValue(childTasks); + + const results = await searchTasks("optimitron:dev", { + limit: 20, + userId: "user-a", + }); + + expect(results).toHaveLength(20); + expect(results[0]?.taskKey).toBe("optimitron:dev"); + expect(mocks.prisma.taskFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + AND: expect.arrayContaining([{ taskKey: "optimitron:dev" }]), + }), + }), + ); }); it("searchTasks with a user searches public tasks plus that user's created private tasks", async () => { diff --git a/packages/web/src/lib/mcp-instructions.ts b/packages/web/src/lib/mcp-instructions.ts index 9cf24291a..c99c26ab9 100644 --- a/packages/web/src/lib/mcp-instructions.ts +++ b/packages/web/src/lib/mcp-instructions.ts @@ -11,7 +11,9 @@ START HERE (in order): 2. getQueueAudit — data-health check of the personal queue; repair high-severity issues before trusting rankings. 3. getNextAction or getMyQueue — the user's own ranked next actions. getAIQueue — tasks marked for autonomous agent execution. getNextTask — best public task for an anonymous agent. -CREATING WORK: always searchTasks first (visibility defaults to 'all' — public plus your private work — when signed in; pass 'public' or 'private' to narrow). createTask requires an explicit parentTaskId — choose the closest existing objective; never attach directly to Optimize Earth. Estimate value/p_success/hours honestly; a calibrated guess beats omission. Use proposeTaskBundle for multi-task drafts (it runs duplicate review). +CREATING WORK: always searchTasks first (visibility defaults to 'all' — public plus your private work — when signed in; pass 'public' or 'private' to narrow). createTask requires an explicit parentTaskId or exact parentTaskKey — choose the closest existing objective; never attach directly to Optimize Earth. Estimate value/p_success/hours honestly; a calibrated guess beats omission. Use proposeTaskBundle for multi-task drafts (it runs duplicate review). + +OPTIMITRON DEVELOPMENT: for an improvement to Optimitron itself, searchTasks with query "optimitron:dev" and visibility "all". Confirm the exact taskKey "optimitron:dev", search again for duplicate work, then call createTask with parentTaskKey "optimitron:dev". If the development root is not accessible, stop instead of attaching the task somewhere else. COMPLETING WORK: for a private uncompensated Self task you own, call completeTask once with factual completion evidence; it self-verifies the task and removes it from your active queue. For delegated, shared, paid, public, organization, or agent work, use startTaskExecution → submitTaskArtifact → submitTaskForVerification, then wait for authorized verification. completeTaskClaim only submits one claim for review and never completes the task itself. diff --git a/packages/web/src/lib/mcp-server.ts b/packages/web/src/lib/mcp-server.ts index 4d93ccbdc..69f13c7d4 100644 --- a/packages/web/src/lib/mcp-server.ts +++ b/packages/web/src/lib/mcp-server.ts @@ -2256,22 +2256,26 @@ function enrichTaskForMcp(task: unknown) { } async function validateExplicitTaskParent(input: { - parentTaskId: string; + clientAccessBoundary: TaskClientAccessBoundary; + parentTaskId?: string | null; + parentTaskKey?: string | null; prisma: Awaited>; taskId?: string; userId: string; }) { - if (input.parentTaskId === OPTIMIZE_EARTH_ROOT_TASK_ID) { - throw new Error( - "Optimize Earth is reserved for managed top-level branches. Choose the closest existing objective or task instead.", - ); + if (!input.parentTaskId && !input.parentTaskKey) { + throw new Error("A parent task ID or key is required."); } const parent = await input.prisma.task.findFirst({ where: { deletedAt: null, - id: input.parentTaskId, - ...getTaskAccessWhere({ action: "COMMENT", userId: input.userId }), + ...(input.parentTaskId ? { id: input.parentTaskId } : {}), + ...(input.parentTaskKey ? { taskKey: input.parentTaskKey } : {}), + AND: [ + getTaskAccessWhere({ action: "COMMENT", userId: input.userId }), + getTaskClientAccessWhere(input.clientAccessBoundary), + ], }, select: { assigneeOrganizationId: true, @@ -2284,8 +2288,14 @@ async function validateExplicitTaskParent(input: { }, }); if (!parent) { + const parentRef = input.parentTaskId ?? input.parentTaskKey; throw new Error( - `Parent task ${JSON.stringify(input.parentTaskId)} was not found. Search the existing task tree and choose a valid parent.`, + `Parent task ${JSON.stringify(parentRef)} was not found. Search the existing task tree and choose a valid accessible parent.`, + ); + } + if (parent.id === OPTIMIZE_EARTH_ROOT_TASK_ID) { + throw new Error( + "Optimize Earth is reserved for managed top-level branches. Choose the closest existing objective or task instead.", ); } if (input.taskId === parent.id) { @@ -6390,7 +6400,8 @@ const TASK_TOOL_DEFINITIONS = [ name: "createTask", description: "Create a task. Visibility defaults to PRIVATE; admin callers get PUBLIC by default when assigneeOrganizationId is set so leader/president/treaty-activation tasks land on the public Earth feed. PUBLIC tasks and PUBLIC organization-assigned defaults are admin-only; pass visibility='PRIVATE' or 'PUBLIC' to override. Non-admin callers requesting PUBLIC get rejected. Tasks default to ACTIVE so they appear in the relevant queue immediately. " + - "Required: title, description, parentTaskId, taskKey, category, hours, value, p_success, acceptanceCriteria, impactStatement. Call searchTasks or listTasks first and choose the closest existing parent; Optimize Earth itself is reserved for managed top-level branches. Every required field is load-bearing — a task that omits one either fails validation or lands at score 0 and never surfaces. " + + "Required: title, description, one of parentTaskId or parentTaskKey, taskKey, category, hours, value, p_success, acceptanceCriteria, impactStatement. Call searchTasks or listTasks first and choose the closest existing parent; Optimize Earth itself is reserved for managed top-level branches. Every required field is load-bearing — a task that omits one either fails validation or lands at score 0 and never surfaces. " + + "For Optimitron code or documentation improvements, search for duplicate work and set parentTaskKey='optimitron:dev'. " + "Estimate, don't omit: a calibrated guess with p_success<1 beats no number. State acceptance criteria as a checklist of testable conditions; state impact in one sentence (why this matters). " + "Use depends_on for true prerequisites; executor_type='Self' for user work and 'AI Agent' only for autonomous assistant work; deadline_policy='REQUIRED' for must-do legal/health/safety tasks and 'EXPIRES' for opportunities that vanish after due_at. " + "taskKey is the idempotency key: retrying the same create returns the existing task instead of creating a duplicate. The response includes a writeReceipt and a missingFields[] array for soft-recommended metadata.", @@ -6405,7 +6416,12 @@ const TASK_TOOL_DEFINITIONS = [ parentTaskId: { type: "string", description: - "Required existing parent task ID. Search the accessible task tree first and choose the closest objective or task; do not use Optimize Earth directly.", + "Existing parent task ID. Provide this or parentTaskKey, not both. Search the accessible task tree first and choose the closest objective or task; do not use Optimize Earth directly.", + }, + parentTaskKey: { + type: "string", + description: + "Exact stable key of the existing parent task. Provide this or parentTaskId, not both. For Optimitron development work use 'optimitron:dev'.", }, taskKey: { type: "string", @@ -7958,7 +7974,8 @@ const TASK_TOOL_DEFINITIONS = [ name: "searchTasks", description: "Search your accessible tasks by title, description, task key, assignee, or organization. " + - "Use this before createTask/updateTask when you need blockerTaskIds or blockedTaskIds.", + "Use this before createTask/updateTask to find parents, duplicates, blockerTaskIds, or blockedTaskIds. " + + "For Optimitron code or documentation work, query the stable key 'optimitron:dev', select that exact result, and call createTask with parentTaskKey='optimitron:dev'.", inputSchema: { type: "object" as const, properties: { @@ -10849,7 +10866,8 @@ export function createMcpServer( parseFiniteNumber(a.p_success) != null || parseFiniteNumber(a.pSuccess) != null || parseFiniteNumber(a.successProbabilityBase) != null; - const parentTaskId = optionalString(a.parentTaskId); + const requestedParentTaskId = optionalString(a.parentTaskId); + const parentTaskKey = optionalString(a.parentTaskKey); const taskKey = optionalString(a.taskKey); if (isReservedDocumentReviewTaskKey(taskKey)) { return err(RESERVED_DOCUMENT_REVIEW_TASK_KEY_MESSAGE); @@ -10858,7 +10876,14 @@ export function createMcpServer( if (!title) validationMissingFields.push("title"); if (!description.trim()) validationMissingFields.push("description"); - if (!parentTaskId) validationMissingFields.push("parentTaskId"); + if (!requestedParentTaskId && !parentTaskKey) { + validationMissingFields.push("parentTaskId or parentTaskKey"); + } + if (requestedParentTaskId && parentTaskKey) { + invalidFields.push( + "parentTaskId and parentTaskKey (provide exactly one)", + ); + } if (!taskKey) validationMissingFields.push("taskKey"); if (!a.category) validationMissingFields.push("category"); if (extractedCriteria.length === 0) @@ -10895,7 +10920,9 @@ export function createMcpServer( >; try { parentTask = await validateExplicitTaskParent({ - parentTaskId: parentTaskId!, + clientAccessBoundary: taskClientBoundary, + parentTaskId: requestedParentTaskId, + parentTaskKey, prisma, userId, }); @@ -10904,6 +10931,7 @@ export function createMcpServer( error instanceof Error ? error.message : "Invalid parent task.", ); } + const parentTaskId = parentTask.id; if (taskKey) { const existingTask = await prisma.task.findFirst({ @@ -11133,7 +11161,7 @@ export function createMcpServer( const data: Record = { title: title!, description, - parentTaskId: parentTaskId!, + parentTaskId, taskKey, category: a.category ? TaskCategory[a.category as keyof typeof TaskCategory] @@ -13132,6 +13160,7 @@ export function createMcpServer( >; try { parentTask = await validateExplicitTaskParent({ + clientAccessBoundary: taskClientBoundary, parentTaskId, prisma, taskId: existingTask.id, diff --git a/packages/web/src/lib/tasks.server.ts b/packages/web/src/lib/tasks.server.ts index 4538d2b3c..76cd7e194 100644 --- a/packages/web/src/lib/tasks.server.ts +++ b/packages/web/src/lib/tasks.server.ts @@ -884,20 +884,22 @@ function mapTaskSearchResult( category: task.category, href, id: task.id, - score: scoreSearchRecord(searchTerms, { - title: task.title, - description: snippet, - href, - keywords: [ - task.category, - task.status, - task.taskKey ?? "", - ...task.interestTags, - ...task.skillTags, - ...assigneeParts, - ], - section: "Tasks", - }), + score: + (task.taskKey?.toLowerCase() === searchTerms.normalizedQuery ? 100 : 0) + + scoreSearchRecord(searchTerms, { + title: task.title, + description: snippet, + href, + keywords: [ + task.category, + task.status, + task.taskKey ?? "", + ...task.interestTags, + ...task.skillTags, + ...assigneeParts, + ], + section: "Tasks", + }), snippet: snippet || null, status: task.status, taskKey: task.taskKey, @@ -1696,66 +1698,100 @@ export async function searchTasks( return []; } - const requiredTerms = + const queryTerms = searchTerms.terms.length > 0 ? searchTerms.terms : [searchTerms.normalizedQuery]; - - const tasks = await prisma.task.findMany({ - where: { - AND: [ - getTaskVisibilityWhere({ - userId: options?.userId, - visibility: - options?.visibility ?? (options?.userId ? "accessible" : "public"), - }), - ...(options?.clientAccessBoundary - ? [getTaskClientAccessWhere(options.clientAccessBoundary)] - : []), - ...(options?.status ? [{ status: options.status }] : []), - ...requiredTerms.map((term) => ({ - OR: [ - { title: { contains: term, mode: "insensitive" as const } }, - { - description: { contains: term, mode: "insensitive" as const }, - }, - { taskKey: { contains: term, mode: "insensitive" as const } }, - { roleTitle: { contains: term, mode: "insensitive" as const } }, - { - assigneeOrganization: { - is: { - name: { contains: term, mode: "insensitive" as const }, - }, - }, - }, - { - assigneePerson: { - is: { - displayName: { - contains: term, - mode: "insensitive" as const, - }, - }, - }, - }, - { - assigneePerson: { - is: { - currentAffiliation: { - contains: term, - mode: "insensitive" as const, - }, - }, - }, - }, - ], - })), - ], + const taskSearchIntentTerms = new Set([ + "create", + "find", + "parent", + "search", + "task", + "tasks", + ]); + const distinctiveTerms = queryTerms.filter( + (term) => !taskSearchIntentTerms.has(term), + ); + const candidateTerms = + distinctiveTerms.length > 0 ? distinctiveTerms : queryTerms; + + // Prisma cannot order substring matches by relevance. Fetch a bounded set, + // then let scoreSearchRecord rank title/task-key matches ahead of incidental + // description matches. Requiring every term here made natural agent queries + // such as "find Optimize Optimitron parent" return nothing even though the + // distinctive terms matched the canonical task. + const candidateLimit = Math.min(Math.max(limit * 4, 128), 500); + const termMatches = candidateTerms.flatMap((term) => [ + { title: { contains: term, mode: "insensitive" as const } }, + { description: { contains: term, mode: "insensitive" as const } }, + { taskKey: { contains: term, mode: "insensitive" as const } }, + { roleTitle: { contains: term, mode: "insensitive" as const } }, + { + assigneeOrganization: { + is: { + name: { contains: term, mode: "insensitive" as const }, + }, + }, }, - orderBy: [{ verifiedAt: "desc" }, { createdAt: "desc" }], - select: taskSearchSelect, - take: Math.max(limit * 4, 24), - }); + { + assigneePerson: { + is: { + displayName: { + contains: term, + mode: "insensitive" as const, + }, + }, + }, + }, + { + assigneePerson: { + is: { + currentAffiliation: { + contains: term, + mode: "insensitive" as const, + }, + }, + }, + }, + ]); + + const accessFilters: Prisma.TaskWhereInput[] = [ + getTaskVisibilityWhere({ + userId: options?.userId, + visibility: + options?.visibility ?? (options?.userId ? "accessible" : "public"), + }), + ...(options?.clientAccessBoundary + ? [getTaskClientAccessWhere(options.clientAccessBoundary)] + : []), + ...(options?.status ? [{ status: options.status }] : []), + ]; + const exactTaskKeyPromise = searchTerms.normalizedQuery.includes(":") + ? prisma.task.findFirst({ + where: { + AND: [...accessFilters, { taskKey: searchTerms.normalizedQuery }], + }, + select: taskSearchSelect, + }) + : Promise.resolve(null); + const [exactTaskKeyMatch, candidates] = await Promise.all([ + exactTaskKeyPromise, + prisma.task.findMany({ + where: { + AND: [...accessFilters, { OR: termMatches }], + }, + orderBy: [{ verifiedAt: "desc" }, { createdAt: "desc" }], + select: taskSearchSelect, + take: candidateLimit, + }), + ]); + const tasks = exactTaskKeyMatch + ? [ + exactTaskKeyMatch, + ...candidates.filter((task) => task.id !== exactTaskKeyMatch.id), + ] + : candidates; return tasks .map((task) => mapTaskSearchResult(task, searchTerms)) From f5e1251ab5bca4f3c9a52d3ff275b752559eb11e Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Fri, 31 Jul 2026 19:44:41 -0500 Subject: [PATCH 2/3] Keep task search candidates relevant --- .../src/lib/__tests__/tasks.server.test.ts | 25 ++++--- packages/web/src/lib/tasks.server.ts | 66 ++++++++++--------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/packages/web/src/lib/__tests__/tasks.server.test.ts b/packages/web/src/lib/__tests__/tasks.server.test.ts index 502975389..73a925598 100644 --- a/packages/web/src/lib/__tests__/tasks.server.test.ts +++ b/packages/web/src/lib/__tests__/tasks.server.test.ts @@ -719,7 +719,7 @@ describe("tasks server", () => { ]); }); - it("searchTasks without a user searches public tasks using OR-ranked terms", async () => { + it("searchTasks ignores request-framing words but requires every distinctive term", async () => { await searchTasks("find secret grant memo task", { userId: null }); const args = lastTaskFindManyArgs(); @@ -727,20 +727,19 @@ describe("tasks server", () => { expect(filters[0]).toEqual( expect.objectContaining({ deletedAt: null, isPublic: true }), ); - expect(filters.slice(1)).toHaveLength(1); - const termFilter = filters[1] as { OR: unknown[] }; + expect(filters.slice(1)).toHaveLength(3); for (const term of ["secret", "grant", "memo"]) { - expect(termFilter.OR).toContainEqual({ - title: { contains: term, mode: "insensitive" }, - }); + expect(filters.slice(1)).toContainEqual( + expect.objectContaining({ + OR: expect.arrayContaining([ + { title: { contains: term, mode: "insensitive" } }, + ]), + }), + ); } - expect(termFilter.OR).not.toContainEqual({ - title: { contains: "find", mode: "insensitive" }, - }); - expect(termFilter.OR).not.toContainEqual({ - title: { contains: "task", mode: "insensitive" }, - }); - expect(args.take).toBe(128); + expect(JSON.stringify(filters.slice(1))).not.toContain('"find"'); + expect(JSON.stringify(filters.slice(1))).not.toContain('"task"'); + expect(args.take).toBe(64); }); it.each([ diff --git a/packages/web/src/lib/tasks.server.ts b/packages/web/src/lib/tasks.server.ts index 76cd7e194..f844864d3 100644 --- a/packages/web/src/lib/tasks.server.ts +++ b/packages/web/src/lib/tasks.server.ts @@ -1716,45 +1716,47 @@ export async function searchTasks( const candidateTerms = distinctiveTerms.length > 0 ? distinctiveTerms : queryTerms; - // Prisma cannot order substring matches by relevance. Fetch a bounded set, - // then let scoreSearchRecord rank title/task-key matches ahead of incidental - // description matches. Requiring every term here made natural agent queries - // such as "find Optimize Optimitron parent" return nothing even though the - // distinctive terms matched the canonical task. - const candidateLimit = Math.min(Math.max(limit * 4, 128), 500); - const termMatches = candidateTerms.flatMap((term) => [ - { title: { contains: term, mode: "insensitive" as const } }, - { description: { contains: term, mode: "insensitive" as const } }, - { taskKey: { contains: term, mode: "insensitive" as const } }, - { roleTitle: { contains: term, mode: "insensitive" as const } }, - { - assigneeOrganization: { - is: { - name: { contains: term, mode: "insensitive" as const }, + // Prisma cannot order substring matches by relevance. Require every + // distinctive term at the database boundary, then rank the bounded matches + // in memory. Ignoring request-framing words keeps natural agent queries such + // as "find Optimize Optimitron parent" from failing on the word "parent" + // without letting a single common term flood the candidate window. + const candidateLimit = Math.min(Math.max(limit * 4, 64), 500); + const requiredTermMatches = candidateTerms.map((term) => ({ + OR: [ + { title: { contains: term, mode: "insensitive" as const } }, + { description: { contains: term, mode: "insensitive" as const } }, + { taskKey: { contains: term, mode: "insensitive" as const } }, + { roleTitle: { contains: term, mode: "insensitive" as const } }, + { + assigneeOrganization: { + is: { + name: { contains: term, mode: "insensitive" as const }, + }, }, }, - }, - { - assigneePerson: { - is: { - displayName: { - contains: term, - mode: "insensitive" as const, + { + assigneePerson: { + is: { + displayName: { + contains: term, + mode: "insensitive" as const, + }, }, }, }, - }, - { - assigneePerson: { - is: { - currentAffiliation: { - contains: term, - mode: "insensitive" as const, + { + assigneePerson: { + is: { + currentAffiliation: { + contains: term, + mode: "insensitive" as const, + }, }, }, }, - }, - ]); + ], + })); const accessFilters: Prisma.TaskWhereInput[] = [ getTaskVisibilityWhere({ @@ -1779,7 +1781,7 @@ export async function searchTasks( exactTaskKeyPromise, prisma.task.findMany({ where: { - AND: [...accessFilters, { OR: termMatches }], + AND: [...accessFilters, ...requiredTermMatches], }, orderBy: [{ verifiedAt: "desc" }, { createdAt: "desc" }], select: taskSearchSelect, From 1f757e7fdbd901a1aa8e54d5f1a05586142a4cb6 Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Fri, 31 Jul 2026 19:49:55 -0500 Subject: [PATCH 3/3] Clarify MCP parent key syntax --- packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts | 2 +- packages/web/src/lib/mcp-instructions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts b/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts index 4972100fe..d77548b5f 100644 --- a/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts +++ b/packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts @@ -80,6 +80,6 @@ describe("MCP tool catalog", () => { }); it("tells coding agents how to target the canonical development branch", () => { - expect(MCP_SERVER_INSTRUCTIONS).toContain('parentTaskKey "optimitron:dev"'); + expect(MCP_SERVER_INSTRUCTIONS).toContain("parentTaskKey='optimitron:dev'"); }); }); diff --git a/packages/web/src/lib/mcp-instructions.ts b/packages/web/src/lib/mcp-instructions.ts index c99c26ab9..506a6af68 100644 --- a/packages/web/src/lib/mcp-instructions.ts +++ b/packages/web/src/lib/mcp-instructions.ts @@ -13,7 +13,7 @@ START HERE (in order): CREATING WORK: always searchTasks first (visibility defaults to 'all' — public plus your private work — when signed in; pass 'public' or 'private' to narrow). createTask requires an explicit parentTaskId or exact parentTaskKey — choose the closest existing objective; never attach directly to Optimize Earth. Estimate value/p_success/hours honestly; a calibrated guess beats omission. Use proposeTaskBundle for multi-task drafts (it runs duplicate review). -OPTIMITRON DEVELOPMENT: for an improvement to Optimitron itself, searchTasks with query "optimitron:dev" and visibility "all". Confirm the exact taskKey "optimitron:dev", search again for duplicate work, then call createTask with parentTaskKey "optimitron:dev". If the development root is not accessible, stop instead of attaching the task somewhere else. +OPTIMITRON DEVELOPMENT: for an improvement to Optimitron itself, searchTasks with query "optimitron:dev" and visibility "all". Confirm the exact taskKey "optimitron:dev", search again for duplicate work, then call createTask with parentTaskKey='optimitron:dev'. If the development root is not accessible, stop instead of attaching the task somewhere else. COMPLETING WORK: for a private uncompensated Self task you own, call completeTask once with factual completion evidence; it self-verifies the task and removes it from your active queue. For delegated, shared, paid, public, organization, or agent work, use startTaskExecution → submitTaskArtifact → submitTaskForVerification, then wait for authorized verification. completeTaskClaim only submits one claim for review and never completes the task itself.