suggest-task-placement
Classify new tasks into existing project headings by analyzing task titles and project structure to maintain organization in Things 3.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| project_uuid | Yes | UUID of the project whose existing headings should be reused | |
| task_titles | Yes | Task titles to classify into the project's existing headings |
Implementation Reference
- src/task-placement.ts:78-158 (handler)The main logic for suggesting task placement based on semantic matching between task titles and existing headings/tasks in a project.
export function suggestTaskPlacement(input: { projectTitle: string; headings: TaskLike[]; todos: TaskLike[]; taskTitles: string[]; }) { const corpora = buildHeadingCorpus(input.headings, input.todos); const placements = input.taskTitles.map((taskTitle) => { const taskTokens = tokenize(taskTitle); const scored = corpora .map((heading) => { let score = 0; const matches: string[] = []; for (const token of taskTokens) { if (heading.tokens.has(token)) { score += 3; matches.push(token); } } const normalizedHeading = normalizeHeadingName(heading.headingTitle); if (taskTitle.toLowerCase().includes(normalizedHeading)) { score += 4; matches.push(heading.headingTitle); } if (heading.ownTodoCount > 0 && score > 0) { score += 1; } return { headingId: heading.headingId, headingTitle: heading.headingTitle, score, matches: [...new Set(matches)], }; }) .sort((a, b) => b.score - a.score || a.headingTitle.localeCompare(b.headingTitle)); const best = scored[0]; const second = scored[1]; const ambiguous = Boolean(best && second && best.score > 0 && best.score === second.score); const confident = Boolean(best && best.score >= 3 && !ambiguous); return { taskTitle, suggestedHeadingId: confident ? best.headingId : null, suggestedHeadingTitle: confident ? best.headingTitle : null, confidence: confident ? (best.score >= 6 ? "high" : "medium") : "low", ambiguous, reason: confident ? `Coincide mejor con ${best.headingTitle}${best.matches.length ? ` por: ${best.matches.join(", ")}` : ""}.` : ambiguous ? "La tarea podría encajar en más de un heading existente." : "No hay una coincidencia semántica suficientemente clara con los headings existentes.", alternatives: scored .filter((entry) => entry.score > 0) .slice(0, 3) .map(({ headingId, headingTitle, score }) => ({ headingId, headingTitle, score })), }; }); return { projectTitle: input.projectTitle, headings: corpora.map(({ headingId, headingTitle, ownTodoCount }) => ({ headingId, headingTitle, ownTodoCount, })), placements, summary: { totalTasks: placements.length, confidentlyPlaced: placements.filter((entry) => entry.suggestedHeadingId).length, needsReview: placements.filter((entry) => !entry.suggestedHeadingId).length, }, guidance: "Usa estas sugerencias para reutilizar los headings existentes del proyecto. Si una tarea queda con confianza baja o ambigua, conviene confirmar con el usuario antes de moverla.", }; } - src/index.ts:1588-1609 (registration)Registration of the suggest-task-placement tool, which orchestrates the call to suggestTaskPlacement in src/task-placement.ts.
"suggest-task-placement", { project_uuid: z.string().describe("UUID of the project whose existing headings should be reused"), task_titles: z.array(z.string()).min(1).describe("Task titles to classify into the project's existing headings"), }, async ({ project_uuid, task_titles }) => { const placement = await withDatabase((db) => { const structure = buildProjectStructure(getAllTasks(db), project_uuid); return suggestTaskPlacement({ projectTitle: structure.project.title, headings: structure.headings, todos: structure.todos, taskTitles: task_titles, }); }); return buildTextResponse( `Suggested placement for ${placement.summary.totalTasks} tasks in ${placement.projectTitle}`, placement ); } );