feat(todo): allow several in_progress todos at once

Remove the single-in_progress cap from todo_write execute validation and
the durable-log invariant so a task list can mirror genuinely parallel
work (concurrent subagents, background commands). Update the tool
description to instruct marking every actively worked task in_progress,
refresh the tool catalog and keyless snapshot expected outputs, and
record the decision in a new Agent Note superseding the original cap.
This commit is contained in:
Chinesezjc
2026-07-26 02:50:47 +08:00
parent 6f50208ea3
commit 876065a97d
44 changed files with 150 additions and 68 deletions

View File

@@ -19,22 +19,24 @@ const DESCRIPTION =
'Record and update a structured task list for the current work. Send the ENTIRE '
+ 'list every call — it REPLACES the previous list (there are no partial updates, '
+ 'no per-item edits). Use it to plan multi-step work and show progress: add one '
+ 'todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` '
+ 'at a time; while work remains, exactly one active task should be '
+ '`in_progress`. Mark a todo `completed` the moment it is done (do not batch '
+ 'completions), and allow no `in_progress` item only once all work is complete. '
+ 'Skip the list for trivial single-step tasks. Statuses: `pending` '
+ '(not started), `in_progress` (being worked on now), `completed` (finished).'
+ 'todo per concrete step before you start. Mark every todo being actively worked '
+ 'on `in_progress` — several at once when work genuinely runs in parallel (e.g. '
+ 'concurrent subagents or background commands), one for sequential work; while '
+ 'work remains, at least one task should be `in_progress`. Mark a todo '
+ '`completed` the moment it is done (do not batch completions), and allow no '
+ '`in_progress` item only once all work is complete. Skip the list for trivial '
+ 'single-step tasks. Statuses: `pending` (not started), `in_progress` (being '
+ 'worked on now), `completed` (finished).'
/**
* Validate the value constraints the ParameterSchemaSpec can't express and build the canonical {@link
* TodoItem}[]: trimmed non-empty unique content and at most one in-progress item. The registry
* has already enforced the status enum; the cast below records that guarantee.
* TodoItem}[]: trimmed non-empty unique content. Any number of items may be in_progress —
* parallel work (subagents, background commands) legitimately runs several tasks at once. The
* registry has already enforced the status enum; the cast below records that guarantee.
*/
function toTodoList(raw: { content: string; status: string }[]): TodoItem[] {
const todos: TodoItem[] = []
const seen = new Set<string>()
let inProgress = 0
for (const item of raw) {
const content = item.content.trim()
if (content.length === 0) {
@@ -44,12 +46,7 @@ function toTodoList(raw: { content: string; status: string }[]): TodoItem[] {
throw new Error(`invalid todos: duplicate content ${JSON.stringify(content)}`)
}
seen.add(content)
const status = item.status as TodoItem['status']
if (status === 'in_progress') inProgress++
todos.push({ content, status })
}
if (inProgress > 1) {
throw new Error(`invalid todos: at most one task may be in_progress, got ${inProgress}`)
todos.push({ content, status: item.status as TodoItem['status'] })
}
return todos
}

View File

@@ -16,7 +16,6 @@ export const inject = ['invariants']
function validateTodos(value: unknown, fail: InvariantFailure): void {
if (!Array.isArray(value)) fail('todo/write todos must be an array')
const seen = new Set<string>()
let active = 0
for (const item of value) {
if (typeof item !== 'object' || item === null) fail('todo/write entries must be objects')
const { content, status } = item as Record<string, unknown>
@@ -28,9 +27,7 @@ function validateTodos(value: unknown, fail: InvariantFailure): void {
if (typeof status !== 'string' || !TODO_STATUSES.has(status)) {
fail(`todo/write carries unknown status ${JSON.stringify(status)}`)
}
if (status === 'in_progress') active += 1
}
if (active > 1) fail(`todo/write contains ${active} in-progress entries; at most one is allowed`)
}
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */