@comfanion/usethis_todo 0.1.16-dev.1 → 0.1.16-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +5 -29
- package/package.json +1 -1
- package/tools.ts +121 -116
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
2
2
|
import path from "path"
|
|
3
3
|
import fs from "fs/promises"
|
|
4
4
|
|
|
5
|
-
import { write, read, read_five, read_by_id,
|
|
5
|
+
import { write, read, read_five, read_by_id, setNativeStorageBase } from "./tools"
|
|
6
6
|
|
|
7
7
|
interface TodoPruneState {
|
|
8
8
|
lastToolCallId: string | null
|
|
@@ -38,12 +38,9 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
38
38
|
|
|
39
39
|
const prunedToolNames = new Set([
|
|
40
40
|
"usethis_todo_write",
|
|
41
|
-
"usethis_todo_update",
|
|
42
41
|
"usethis_todo_read",
|
|
43
42
|
"usethis_todo_read_five",
|
|
44
43
|
"usethis_todo_read_by_id",
|
|
45
|
-
"todowrite",
|
|
46
|
-
"todoread",
|
|
47
44
|
])
|
|
48
45
|
|
|
49
46
|
// Collect all TODO-related tool parts
|
|
@@ -69,16 +66,16 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
69
66
|
},
|
|
70
67
|
|
|
71
68
|
tool: {
|
|
72
|
-
//
|
|
73
|
-
//
|
|
69
|
+
// Custom names — visible output in TUI
|
|
70
|
+
// NOTE: native "todowrite"/"todoread" names trigger sidebar refresh
|
|
71
|
+
// but TUI hides their output. Using custom names for visibility.
|
|
74
72
|
usethis_todo_write: write,
|
|
75
73
|
usethis_todo_read: read,
|
|
76
74
|
usethis_todo_read_five: read_five,
|
|
77
75
|
usethis_todo_read_by_id: read_by_id,
|
|
78
|
-
usethis_todo_update: update,
|
|
79
76
|
},
|
|
80
77
|
|
|
81
|
-
// Set nicer titles in TUI + track prune state
|
|
78
|
+
// Set nicer titles in TUI + track prune state
|
|
82
79
|
"tool.execute.after": async (input, output) => {
|
|
83
80
|
if (!input.tool.startsWith("usethis_todo_")) return
|
|
84
81
|
|
|
@@ -96,9 +93,6 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
96
93
|
if (input.tool === "usethis_todo_write") {
|
|
97
94
|
const match = out.match(/\[(\d+)\/(\d+) done/)
|
|
98
95
|
output.title = match ? `TODO: ${match[2]} tasks` : "TODO updated"
|
|
99
|
-
} else if (input.tool === "usethis_todo_update") {
|
|
100
|
-
const match = out.match(/^✅ (.+)$/m)
|
|
101
|
-
output.title = match ? match[1] : "Task updated"
|
|
102
96
|
} else if (input.tool === "usethis_todo_read") {
|
|
103
97
|
const match = out.match(/\[(\d+)\/(\d+) done, (\d+) in progress\]/)
|
|
104
98
|
output.title = match ? `TODO [${match[1]}/${match[2]} done]` : "TODO list"
|
|
@@ -107,24 +101,6 @@ const UsethisTodoPlugin: Plugin = async ({ directory, client }) => {
|
|
|
107
101
|
} else if (input.tool === "usethis_todo_read_by_id") {
|
|
108
102
|
output.title = "Task details"
|
|
109
103
|
}
|
|
110
|
-
|
|
111
|
-
// Publish TODO snapshot into chat for write/update ops
|
|
112
|
-
const isWriteOp = input.tool === "usethis_todo_write"
|
|
113
|
-
|| input.tool === "usethis_todo_update"
|
|
114
|
-
if (!isWriteOp) return
|
|
115
|
-
|
|
116
|
-
const text = ["## TODO", "", out].join("\n")
|
|
117
|
-
try {
|
|
118
|
-
await client?.session?.prompt?.({
|
|
119
|
-
path: { id: input.sessionID },
|
|
120
|
-
body: {
|
|
121
|
-
noReply: true,
|
|
122
|
-
parts: [{ type: "text", text }],
|
|
123
|
-
},
|
|
124
|
-
})
|
|
125
|
-
} catch {
|
|
126
|
-
// non-fatal
|
|
127
|
-
}
|
|
128
104
|
},
|
|
129
105
|
}
|
|
130
106
|
}
|
package/package.json
CHANGED
package/tools.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TODO Tool with Dependencies & Priority —
|
|
2
|
+
* TODO Tool with Dependencies & Priority — v4 (unified storage)
|
|
3
3
|
*
|
|
4
|
-
* 4
|
|
5
|
-
* usethis_todo_write({ todos
|
|
6
|
-
* usethis_todo_read()
|
|
7
|
-
* usethis_todo_read_five()
|
|
8
|
-
* usethis_todo_read_by_id()
|
|
9
|
-
* usethis_todo_update(id, field, value) - update any task field
|
|
4
|
+
* 4 tools:
|
|
5
|
+
* usethis_todo_write({ todos, merge? }) - create/update TODO list (upsert by default)
|
|
6
|
+
* usethis_todo_read() - read TODO with graph analysis
|
|
7
|
+
* usethis_todo_read_five() - read next 5 tasks with their blockers
|
|
8
|
+
* usethis_todo_read_by_id({ id }) - read task by id with its blockers
|
|
10
9
|
*
|
|
11
10
|
* Storage:
|
|
12
11
|
* Unified: Native OpenCode storage (TUI compatible)
|
|
@@ -16,6 +15,7 @@
|
|
|
16
15
|
* - Stores FULL enhanced schema (blockedBy, priority, etc) in the native file
|
|
17
16
|
* - Maps statuses to native values (pending/completed) for Sidebar compatibility
|
|
18
17
|
* - Preserves original statuses in `usethisStatus` field
|
|
18
|
+
* - merge (default: true) = upsert; merge: false = replace entire list
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { tool } from "@opencode-ai/plugin"
|
|
@@ -71,14 +71,14 @@ interface Todo {
|
|
|
71
71
|
content: string // Short task summary
|
|
72
72
|
description?: string // Full task description (optional)
|
|
73
73
|
releases?: string[] // Release identifiers (optional)
|
|
74
|
-
status: string //
|
|
74
|
+
status: string // pending | in_progress | ready | completed | cancelled
|
|
75
75
|
priority: string // CRIT | HIGH | MED | LOW
|
|
76
76
|
blockedBy?: string[] // IDs of blocking tasks
|
|
77
77
|
createdAt?: number
|
|
78
78
|
updatedAt?: number
|
|
79
79
|
|
|
80
80
|
// Shadow fields for native compatibility
|
|
81
|
-
usethisStatus?: string // The REAL status (
|
|
81
|
+
usethisStatus?: string // The REAL status (pending, ready, etc)
|
|
82
82
|
usethisPriority?: string // The REAL priority (CRIT, HIGH, etc)
|
|
83
83
|
|
|
84
84
|
// Mind extensions (backward compatible - optional)
|
|
@@ -132,22 +132,28 @@ async function logAction(directory: string, action: string, details: string): Pr
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
async function getNativeDataDirs(): Promise<string[]> {
|
|
135
|
-
const dirs =
|
|
136
|
-
|
|
137
|
-
// 1) xdg-basedir (what OpenCode itself uses)
|
|
138
|
-
// Removed dynamic import to avoid "chunk not found" errors in some environments
|
|
139
|
-
// relying on standard env vars and paths instead
|
|
135
|
+
const dirs: string[] = []
|
|
136
|
+
const seen = new Set<string>()
|
|
140
137
|
|
|
141
|
-
//
|
|
138
|
+
// 1) explicit XDG override (highest priority)
|
|
142
139
|
if (process.env.XDG_DATA_HOME) {
|
|
143
|
-
dirs.
|
|
140
|
+
dirs.push(process.env.XDG_DATA_HOME)
|
|
141
|
+
seen.add(process.env.XDG_DATA_HOME)
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
// 2) common fallbacks
|
|
145
|
+
const fallbacks = [
|
|
146
|
+
path.join(os.homedir(), ".local", "share"),
|
|
147
|
+
path.join(os.homedir(), "Library", "Application Support"),
|
|
148
|
+
]
|
|
149
|
+
for (const dir of fallbacks) {
|
|
150
|
+
if (!seen.has(dir)) {
|
|
151
|
+
dirs.push(dir)
|
|
152
|
+
seen.add(dir)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
149
155
|
|
|
150
|
-
return
|
|
156
|
+
return dirs
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
async function getStoragePaths(sid: string): Promise<string[]> {
|
|
@@ -172,30 +178,7 @@ async function getStoragePaths(sid: string): Promise<string[]> {
|
|
|
172
178
|
// Mapping Logic (Enhanced <-> Native)
|
|
173
179
|
// ============================================================================
|
|
174
180
|
|
|
175
|
-
function toNativeStatus(status: string): string {
|
|
176
|
-
switch (status) {
|
|
177
|
-
case "todo": return "pending"
|
|
178
|
-
case "in_progress": return "in_progress"
|
|
179
|
-
case "ready": return "in_progress" // Sidebar has no 'ready'
|
|
180
|
-
case "done": return "completed"
|
|
181
|
-
case "cancelled": return "cancelled"
|
|
182
|
-
default: return "pending"
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
181
|
|
|
186
|
-
function fromNativeStatus(nativeStatus: string, originalStatus?: string): string {
|
|
187
|
-
// If we have the original status stored, prefer it
|
|
188
|
-
if (originalStatus) return originalStatus
|
|
189
|
-
|
|
190
|
-
// Otherwise map back (lossy for ready/todo distinction)
|
|
191
|
-
switch (nativeStatus) {
|
|
192
|
-
case "pending": return "todo"
|
|
193
|
-
case "in_progress": return "in_progress"
|
|
194
|
-
case "completed": return "done"
|
|
195
|
-
case "cancelled": return "cancelled"
|
|
196
|
-
default: return "todo"
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
182
|
|
|
200
183
|
function toNativePriority(priority: string): string {
|
|
201
184
|
switch (priority) {
|
|
@@ -214,13 +197,13 @@ function toNativePriority(priority: string): string {
|
|
|
214
197
|
async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
|
|
215
198
|
try {
|
|
216
199
|
const storagePaths = await getStoragePaths(sid)
|
|
217
|
-
// Try to read from any available path, starting with authoritative
|
|
218
200
|
for (const storagePath of storagePaths) {
|
|
219
201
|
try {
|
|
220
202
|
const raw = JSON.parse(await fs.readFile(storagePath, "utf-8"))
|
|
221
203
|
if (Array.isArray(raw)) {
|
|
222
204
|
return raw.map((t: any) => {
|
|
223
|
-
|
|
205
|
+
// Restore "ready" status if it was saved
|
|
206
|
+
const realStatus = t.usethisStatus || t.status
|
|
224
207
|
const realPriority = t.usethisPriority || t.priority
|
|
225
208
|
return normalizeTodo({ ...t, status: realStatus, priority: realPriority })
|
|
226
209
|
})
|
|
@@ -243,15 +226,17 @@ async function writeTodos(todos: Todo[], sid: string, directory?: string): Promi
|
|
|
243
226
|
const enhancedPath = getEnhancedPath(sid, directory)
|
|
244
227
|
|
|
245
228
|
const storageTodos = todos.map(t => {
|
|
246
|
-
|
|
229
|
+
// Status is already native — map "ready" to "in_progress" for native storage
|
|
230
|
+
const nativeStatus = t.status === "ready" ? "in_progress" : t.status
|
|
247
231
|
const nativePriority = toNativePriority(t.priority)
|
|
248
232
|
|
|
249
233
|
return {
|
|
250
234
|
...t,
|
|
251
235
|
status: nativeStatus,
|
|
252
236
|
priority: nativePriority,
|
|
253
|
-
|
|
254
|
-
|
|
237
|
+
usethisPriority: t.priority,
|
|
238
|
+
// Keep "ready" status in a field so we can restore it on read
|
|
239
|
+
...(t.status === "ready" ? { usethisStatus: "ready" } : {}),
|
|
255
240
|
}
|
|
256
241
|
})
|
|
257
242
|
|
|
@@ -289,11 +274,11 @@ function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
|
289
274
|
const availableTodos: Todo[] = []
|
|
290
275
|
|
|
291
276
|
for (const todo of todos) {
|
|
292
|
-
if (normalizeStatus(todo.status) !== "
|
|
277
|
+
if (normalizeStatus(todo.status) !== "pending") continue
|
|
293
278
|
const activeBlockers = (todo.blockedBy || []).filter(id => {
|
|
294
279
|
const b = todos.find(t => t.id === id)
|
|
295
280
|
const bs = normalizeStatus(b?.status)
|
|
296
|
-
return b && bs !== "
|
|
281
|
+
return b && bs !== "completed" && bs !== "cancelled"
|
|
297
282
|
})
|
|
298
283
|
if (activeBlockers.length === 0) {
|
|
299
284
|
availableTodos.push(todo)
|
|
@@ -333,24 +318,24 @@ function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
|
333
318
|
// ============================================================================
|
|
334
319
|
|
|
335
320
|
const PE = (p?: string) => p === "CRIT" ? "🔴" : p === "HIGH" ? "🟠" : p === "LOW" ? "🟢" : "🟡"
|
|
336
|
-
const SI = (s: string) => s === "
|
|
321
|
+
const SI = (s: string) => s === "completed" ? "✓" : s === "in_progress" ? "⚙" : s === "ready" ? "⏳" : s === "cancelled" ? "✗" : s === "pending" ? "○" : "·"
|
|
337
322
|
|
|
338
323
|
function normalizeStatus(input: unknown): string {
|
|
339
324
|
const s = String(input || "").trim()
|
|
340
325
|
|
|
341
|
-
//
|
|
342
|
-
if (s === "
|
|
326
|
+
// Canonical set (aligned with native OpenCode)
|
|
327
|
+
if (s === "pending" || s === "in_progress" || s === "completed" || s === "cancelled") return s
|
|
343
328
|
|
|
344
|
-
//
|
|
345
|
-
if (s === "
|
|
346
|
-
if (s === "waiting_review" || s === "finished") return "ready"
|
|
347
|
-
if (s === "completed") return "done"
|
|
329
|
+
// Our extension
|
|
330
|
+
if (s === "ready") return "ready"
|
|
348
331
|
|
|
349
|
-
//
|
|
350
|
-
if (s === "
|
|
332
|
+
// Back-compat (older versions used different names)
|
|
333
|
+
if (s === "todo") return "pending"
|
|
334
|
+
if (s === "done" || s === "finished") return "completed"
|
|
335
|
+
if (s === "waiting_review") return "ready"
|
|
351
336
|
|
|
352
337
|
// Default
|
|
353
|
-
return "
|
|
338
|
+
return "pending"
|
|
354
339
|
}
|
|
355
340
|
|
|
356
341
|
function normalizeReleases(input: unknown): string[] | undefined {
|
|
@@ -365,8 +350,8 @@ function normalizeTodo(input: any): Todo {
|
|
|
365
350
|
const status = normalizeStatus(input?.status)
|
|
366
351
|
const releases = normalizeReleases(input?.releases)
|
|
367
352
|
|
|
368
|
-
// Auto transition: ready ->
|
|
369
|
-
const promotedStatus = status === "ready" && releases?.length ? "
|
|
353
|
+
// Auto transition: ready -> completed when releases exist
|
|
354
|
+
const promotedStatus = status === "ready" && releases?.length ? "completed" : status
|
|
370
355
|
|
|
371
356
|
// Initialize Mind extensions if provided (backward compatible)
|
|
372
357
|
const normalized: Todo = {
|
|
@@ -401,13 +386,13 @@ function sortTodosForList(todos: Todo[]): Todo[] {
|
|
|
401
386
|
|
|
402
387
|
function isBlocked(todo: Todo, byId: Map<string, Todo>): boolean {
|
|
403
388
|
const s = normalizeStatus(todo.status)
|
|
404
|
-
if (s !== "
|
|
389
|
+
if (s !== "pending") return false
|
|
405
390
|
if (!todo.blockedBy?.length) return false
|
|
406
391
|
for (const id of todo.blockedBy) {
|
|
407
392
|
const b = byId.get(id)
|
|
408
393
|
const bs = normalizeStatus(b?.status)
|
|
409
394
|
if (!b) return true
|
|
410
|
-
if (bs !== "
|
|
395
|
+
if (bs !== "completed" && bs !== "cancelled") return true
|
|
411
396
|
}
|
|
412
397
|
return false
|
|
413
398
|
}
|
|
@@ -508,7 +493,7 @@ function resolveBlockers(todos: Todo[], rootIds: string[]): { blockers: Todo[];
|
|
|
508
493
|
function formatGraph(graph: TodoGraph): string {
|
|
509
494
|
const { todos } = graph
|
|
510
495
|
const total = todos.length
|
|
511
|
-
const done = todos.filter(t => normalizeStatus(t.status) === "
|
|
496
|
+
const done = todos.filter(t => normalizeStatus(t.status) === "completed").length
|
|
512
497
|
const wip = todos.filter(t => normalizeStatus(t.status) === "in_progress").length
|
|
513
498
|
|
|
514
499
|
const availableTodos = graph.available
|
|
@@ -538,37 +523,76 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
538
523
|
// ============================================================================
|
|
539
524
|
|
|
540
525
|
export const write = tool({
|
|
541
|
-
description:
|
|
526
|
+
description: `Create or update TODO list. Use this for TODO. For better performance use ID for task relation.
|
|
527
|
+
|
|
528
|
+
By default (merge: true) this upserts — only specified tasks are created/updated, all others are preserved.
|
|
529
|
+
Set merge: false to replace the entire list.`,
|
|
542
530
|
args: {
|
|
543
531
|
todos: tool.schema.array(
|
|
544
532
|
tool.schema.object({
|
|
545
533
|
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01(for graph dependency)"),
|
|
546
534
|
content: tool.schema.string().describe("Short task summary or title"),
|
|
547
535
|
description: tool.schema.string().optional().describe("Full task description. Helps to remember what should be done"),
|
|
548
|
-
releases: tool.schema.array(tool.schema.string()).optional().describe("Task IDs that auto-promote this task from ready →
|
|
549
|
-
status: tool.schema.string().describe("
|
|
536
|
+
releases: tool.schema.array(tool.schema.string()).optional().describe("Task IDs that auto-promote this task from ready → completed when set"),
|
|
537
|
+
status: tool.schema.string().describe("pending | in_progress | ready | completed | cancelled"),
|
|
550
538
|
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
551
539
|
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks. this help you understand scope better"),
|
|
552
540
|
}),
|
|
553
541
|
).describe("Array of todos"),
|
|
542
|
+
merge: tool.schema.boolean().optional().default(true).describe("When true (default): merge/upsert — only updates specified tasks, preserves others. When false: replaces entire list."),
|
|
554
543
|
},
|
|
555
544
|
async execute(args, context) {
|
|
556
545
|
const now = Date.now()
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
546
|
+
|
|
547
|
+
if (args.merge !== false) {
|
|
548
|
+
// MERGE mode (default): read existing, merge incoming
|
|
549
|
+
const existing = await readTodos(context.sessionID, context.directory)
|
|
550
|
+
const byId = new Map(existing.map(t => [t.id, t]))
|
|
551
|
+
|
|
552
|
+
for (const incoming of args.todos) {
|
|
553
|
+
const normalizedIncoming: any = normalizeTodo(incoming)
|
|
554
|
+
const existingTodo = byId.get(normalizedIncoming.id)
|
|
555
|
+
if (existingTodo) {
|
|
556
|
+
Object.assign(existingTodo, normalizedIncoming)
|
|
557
|
+
existingTodo.updatedAt = now
|
|
558
|
+
existingTodo.status = normalizeTodo(existingTodo).status
|
|
559
|
+
existingTodo.releases = normalizeTodo(existingTodo).releases
|
|
560
|
+
} else {
|
|
561
|
+
byId.set(normalizedIncoming.id, normalizeTodo({ ...normalizedIncoming, createdAt: now, updatedAt: now }))
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const merged = [...byId.values()]
|
|
566
|
+
await writeTodos(merged, context.sessionID, context.directory)
|
|
567
|
+
await logAction(context.directory, "write-merge", `Merged ${args.todos.length} task(s) in session ${context.sessionID}`)
|
|
568
|
+
return formatGraph(analyzeGraph(merged))
|
|
569
|
+
} else {
|
|
570
|
+
// REPLACE mode: replace entire list
|
|
571
|
+
const todos = args.todos.map((t: any) => normalizeTodo({ ...t, createdAt: t.createdAt || now, updatedAt: now }))
|
|
572
|
+
await writeTodos(todos, context.sessionID, context.directory)
|
|
573
|
+
await logAction(context.directory, "write", `Created/Updated ${todos.length} tasks in session ${context.sessionID}`)
|
|
574
|
+
return formatGraph(analyzeGraph(todos))
|
|
575
|
+
}
|
|
561
576
|
},
|
|
562
577
|
})
|
|
563
578
|
|
|
564
579
|
export const read_five = tool({
|
|
565
|
-
description:
|
|
580
|
+
description: `Read the next 5 actionable tasks — unblocked, sorted by priority.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
- Next 5: the top-priority unblocked tasks you can work on right now
|
|
584
|
+
- Blocked By (resolved): full dependency chain for those tasks
|
|
585
|
+
- Count of remaining tasks beyond the top 5
|
|
586
|
+
|
|
587
|
+
Use this as your primary "what should I do next?" tool. More focused than usethis_todo_read() — shows only actionable items without the full list.
|
|
588
|
+
|
|
589
|
+
Call this after completing a task to see what's next.`,
|
|
566
590
|
args: {},
|
|
567
591
|
async execute(_args, context) {
|
|
568
592
|
const todos = await readTodos(context.sessionID, context.directory)
|
|
569
|
-
const ready = sortTodosForList(todos.filter(t => normalizeStatus(t.status) === "
|
|
593
|
+
const ready = sortTodosForList(todos.filter(t => normalizeStatus(t.status) === "pending"))
|
|
570
594
|
await logAction(context.directory, "read_five", `Read next 5 tasks (total ready: ${ready.length})`)
|
|
571
|
-
if (ready.length === 0) return "No tasks in
|
|
595
|
+
if (ready.length === 0) return "No tasks in pending."
|
|
572
596
|
|
|
573
597
|
const items = ready.slice(0, 5)
|
|
574
598
|
|
|
@@ -598,7 +622,20 @@ export const read_five = tool({
|
|
|
598
622
|
})
|
|
599
623
|
|
|
600
624
|
export const read = tool({
|
|
601
|
-
description:
|
|
625
|
+
description: `Read the full TODO list with dependency graph analysis.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
- All Tasks: complete list grouped by epic/story hierarchy with status icons
|
|
629
|
+
- Available Now: unblocked tasks ready to work on, sorted by priority
|
|
630
|
+
- Blocked: tasks waiting on dependencies, showing what blocks them
|
|
631
|
+
- Progress summary: [done/total done, N in progress]
|
|
632
|
+
|
|
633
|
+
Use this to:
|
|
634
|
+
- Review overall progress and plan next steps
|
|
635
|
+
- Understand which tasks are blocked and why
|
|
636
|
+
- Get the full picture before making decisions
|
|
637
|
+
|
|
638
|
+
For a quick view of just the next actionable tasks, use usethis_todo_read_five() instead.`,
|
|
602
639
|
args: {},
|
|
603
640
|
async execute(_args, context) {
|
|
604
641
|
const todos = await readTodos(context.sessionID, context.directory)
|
|
@@ -609,7 +646,14 @@ export const read = tool({
|
|
|
609
646
|
})
|
|
610
647
|
|
|
611
648
|
export const read_by_id = tool({
|
|
612
|
-
description:
|
|
649
|
+
description: `Read a single task by ID with its full dependency chain.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
- Task details: status, priority, description, content
|
|
653
|
+
- Blocked By (resolved): complete chain of blockers (recursive — shows blockers of blockers)
|
|
654
|
+
- Missing dependencies: IDs referenced in blockedBy that don't exist
|
|
655
|
+
|
|
656
|
+
Use this to inspect a specific task's details and understand what's blocking it before starting work.`,
|
|
613
657
|
args: {
|
|
614
658
|
id: tool.schema.string().describe("Task ID"),
|
|
615
659
|
},
|
|
@@ -641,43 +685,4 @@ export const read_by_id = tool({
|
|
|
641
685
|
},
|
|
642
686
|
})
|
|
643
687
|
|
|
644
|
-
export const update = tool({
|
|
645
|
-
description: "Update task(s). Send 1 or many for update",
|
|
646
|
-
args: {
|
|
647
|
-
todos: tool.schema.array(
|
|
648
|
-
tool.schema.object({
|
|
649
|
-
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
|
|
650
|
-
content: tool.schema.string().describe("Short task summary"),
|
|
651
|
-
description: tool.schema.string().optional().describe("Full task description"),
|
|
652
|
-
releases: tool.schema.array(tool.schema.string()).optional().describe("Task IDs that auto-promote this task from ready → done when set (e.g. subtask IDs that were completed)"),
|
|
653
|
-
status: tool.schema.string().describe("todo | in_progress | ready | done"),
|
|
654
|
-
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
655
|
-
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks(from todo -> blocked)"),
|
|
656
|
-
}),
|
|
657
|
-
).describe("Array of todos to update"),
|
|
658
|
-
},
|
|
659
|
-
async execute(args, context) {
|
|
660
|
-
const todos = await readTodos(context.sessionID, context.directory)
|
|
661
|
-
const now = Date.now()
|
|
662
|
-
const byId = new Map(todos.map(t => [t.id, t]))
|
|
663
|
-
|
|
664
|
-
for (const incoming of args.todos) {
|
|
665
|
-
const normalizedIncoming: any = normalizeTodo(incoming)
|
|
666
|
-
const existing = byId.get(normalizedIncoming.id)
|
|
667
|
-
if (existing) {
|
|
668
|
-
Object.assign(existing, normalizedIncoming)
|
|
669
|
-
existing.updatedAt = now
|
|
670
|
-
// Ensure auto transition is applied after merge
|
|
671
|
-
existing.status = normalizeTodo(existing).status
|
|
672
|
-
existing.releases = normalizeTodo(existing).releases
|
|
673
|
-
} else {
|
|
674
|
-
byId.set(normalizedIncoming.id, normalizeTodo({ ...normalizedIncoming, createdAt: now, updatedAt: now }))
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
688
|
|
|
678
|
-
const merged = [...byId.values()]
|
|
679
|
-
await writeTodos(merged, context.sessionID, context.directory)
|
|
680
|
-
await logAction(context.directory, "update", `Updated ${args.todos.length} task(s) in session ${context.sessionID}`)
|
|
681
|
-
return `✅ Updated ${args.todos.length} task(s)\n\n${formatGraph(analyzeGraph(merged))}`
|
|
682
|
-
},
|
|
683
|
-
})
|