diff --git a/TODO.md b/TODO.md index 8f95e109..349b9a5b 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,6 @@ Internal backlog for Orchid. User-facing summary lives in the [README known limi - Interrupted subagents are being marked as complete - possibly after starting a new chain it is not preserved? - but only on some places (subagent view is correct, main agent context and main chat/session UI appears to not be) - replace_symbol can left trailing remnants - crashed / closed app can make the agent lose context (Subagents - on the interface they still appear, for the main againt they do not - IDs not found) -- Remove todo status change restictions - only confuses the agent - Are read results or other tools content being scaped, with the possibility of confusing the agent? ## Agent quality diff --git a/electron/src/main/tools/todo/store.ts b/electron/src/main/tools/todo/store.ts index 2daa766a..3adceb5f 100644 --- a/electron/src/main/tools/todo/store.ts +++ b/electron/src/main/tools/todo/store.ts @@ -1,12 +1,11 @@ /** - * TodoStore — in-memory store for todo tasks with state machine validation. + * TodoStore — in-memory store for todo tasks. * * Ported from Python `src/orchid/domain/todo.py` (TodoStore class). * - * Key behaviors (matching Python): + * Key behaviors: * - Session-scoped in-memory store * - 8-hex UUID generation with collision retry - * - State machine validation via VALID_TRANSITIONS * - create(), get(), list(), update(), delete() * - toData() for serialization (TodoStoreData) * @@ -16,7 +15,6 @@ import { randomUUID } from 'node:crypto'; import { TodoStatus, - VALID_TRANSITIONS, type Todo, type TodoStoreData, } from '../../../shared/types/todo'; @@ -108,8 +106,7 @@ export class TodoStore { /** * Update a task. Returns [task, error]. On success, error is null. * - * Validates status transitions against VALID_TRANSITIONS. - * Terminal status tasks (DONE) cannot be updated. + * No status-transition restrictions — any status can go to any status. * * @param id - Task ID to update * @param updates - Fields to update (title, status, subagent_id) @@ -124,27 +121,6 @@ export class TodoStore { return [null, `No task found with ID '${id}'.`]; } - // DONE is terminal — no transitions allowed (matches Python TERMINAL_STATUSES) - if (task.status === TodoStatus.DONE) { - return [ - null, - `Task '${id}' is in terminal status '${task.status}' and cannot be updated.`, - ]; - } - - // Validate status transition - if (updates.status !== undefined) { - const allowed = VALID_TRANSITIONS[task.status]; - if (!allowed.has(updates.status)) { - const targets = - [...allowed].sort().join(', ') || 'none'; - return [ - null, - `Cannot transition from '${task.status}' to '${updates.status}'. Allowed: ${targets}`, - ]; - } - } - // Apply updates const now = new Date().toISOString(); const updated: Todo = { diff --git a/electron/src/main/tools/todo/update.ts b/electron/src/main/tools/todo/update.ts index 7a78b4f1..3bd2496f 100644 --- a/electron/src/main/tools/todo/update.ts +++ b/electron/src/main/tools/todo/update.ts @@ -39,10 +39,7 @@ export function buildUpdateTool( name: 'todo_update', description: 'Update an existing task owned by the current agent.\n\n' + - 'Status transitions:\n' + - ` OPEN → IN_PROGRESS\n` + - ` IN_PROGRESS → DONE\n` + - ` DONE → (terminal, no transitions)`, + 'Status transitions are unrestricted: any status can be set to any status.', inputSchema: z.object({ id: z.string().describe('The ID of the task to update.'), title: z.string().optional().describe('New title (optional).'), diff --git a/electron/src/shared/types/todo.ts b/electron/src/shared/types/todo.ts index 6d7c88ae..ea562c54 100644 --- a/electron/src/shared/types/todo.ts +++ b/electron/src/shared/types/todo.ts @@ -3,12 +3,8 @@ * * Ported from src/orchid/domain/todo.py. * - * The TodoStore is session-scoped and tracks task state transitions - * via VALID_TRANSITIONS (matching Python's state machine). - * - * Python has 7 statuses; the TS port includes all of them for - * storage-compat. The task description's minimal subset (OPEN, - * IN_PROGRESS, DONE) is the most commonly used. + * The TodoStore is session-scoped. Status is free-form: any status + * can transition to any status (no state-machine restrictions). */ // ── Enums as const objects ────────────────────────────────────────────────── @@ -21,14 +17,6 @@ export const TodoStatus = { export type TodoStatus = (typeof TodoStatus)[keyof typeof TodoStatus]; -// ── Valid transitions ─────────────────────────────────────────────────────── - -export const VALID_TRANSITIONS: Record> = { - [TodoStatus.OPEN]: new Set([TodoStatus.IN_PROGRESS]), - [TodoStatus.IN_PROGRESS]: new Set([TodoStatus.DONE]), - [TodoStatus.DONE]: new Set([]), -}; - // ── Todo ──────────────────────────────────────────────────────────────────── export interface Todo { diff --git a/electron/tests/unit/todo-web-tools.test.ts b/electron/tests/unit/todo-web-tools.test.ts index c517bab8..45f3b550 100644 --- a/electron/tests/unit/todo-web-tools.test.ts +++ b/electron/tests/unit/todo-web-tools.test.ts @@ -2,8 +2,8 @@ * Tests for Todo & Web Tools (U15). * * Covers: - * - Todo: create → ID, OPEN status, OPEN → IN_PROGRESS → DONE (valid), - * DONE → IN_PROGRESS (invalid), list, delete + * - Todo: create → ID, OPEN status, free status transitions (any → any), + * list, delete * - Web fetch: URL validation (scheme/empty only), summarize mode, * raw mode, large content caching */ @@ -203,7 +203,7 @@ describe('Todo Tools', () => { expect(store.get(id)!.status).toBe(TodoStatus.DONE); }); - it('should reject DONE → IN_PROGRESS transition', async () => { + it('should allow DONE → IN_PROGRESS transition (no restrictions)', async () => { const createHandler = buildCreateTool(store).handler; const createResult = (await callTool(createHandler, { title: 'Test', @@ -216,18 +216,18 @@ describe('Todo Tools', () => { await callTool(updateHandler, { id, status: TodoStatus.IN_PROGRESS }); await callTool(updateHandler, { id, status: TodoStatus.DONE }); - // Try to go back to IN_PROGRESS + // Go back to IN_PROGRESS — allowed const result = (await callTool(updateHandler, { id, status: TodoStatus.IN_PROGRESS, })) as ToolExecutionResult; - expect(result.canonical.status).toBe('error'); - expect(result.agentProjection.content).toContain('terminal status'); - expect(store.get(id)!.status).toBe(TodoStatus.DONE); + expect(result.canonical.status).toBe('complete'); + expect(result.agentProjection.content).toContain('IN_PROGRESS'); + expect(store.get(id)!.status).toBe(TodoStatus.IN_PROGRESS); }); - it('should reject OPEN → DONE transition (must go through IN_PROGRESS)', async () => { + it('should allow OPEN → DONE transition directly', async () => { const createHandler = buildCreateTool(store).handler; const createResult = (await callTool(createHandler, { title: 'Test', @@ -240,8 +240,9 @@ describe('Todo Tools', () => { status: TodoStatus.DONE, })) as ToolExecutionResult; - expect(result.canonical.status).toBe('error'); - expect(result.agentProjection.content).toContain('Cannot transition'); + expect(result.canonical.status).toBe('complete'); + expect(result.agentProjection.content).toContain('DONE'); + expect(store.get(id)!.status).toBe(TodoStatus.DONE); }); it('should reject invalid status values at schema boundary', async () => { @@ -288,9 +289,7 @@ describe('Todo Tools', () => { it('should filter by status', async () => { const createHandler = buildCreateTool(store).handler; - const r1 = (await callTool(createHandler, { title: 'Open task' })) as { - content: string; - }; + await callTool(createHandler, { title: 'Open task' }); const r2 = (await callTool(createHandler, { title: 'Progress task' })) as { content: string; };