@comfanion/workflow 4.38.3-dev.2 → 4.38.4-dev.0
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/package.json +1 -1
- package/src/build-info.json +4 -5
- package/src/opencode/config.yaml +0 -69
- package/src/opencode/gitignore +2 -0
- package/src/opencode/opencode.json +3 -5
- package/src/opencode/vectorizer.yaml +45 -0
- package/src/opencode/plugins/README.md +0 -182
- package/src/opencode/plugins/__tests__/custom-compaction.test.ts +0 -829
- package/src/opencode/plugins/__tests__/file-indexer.test.ts +0 -425
- package/src/opencode/plugins/__tests__/helpers/mock-ctx.ts +0 -171
- package/src/opencode/plugins/__tests__/leak-stress.test.ts +0 -315
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +0 -205
- package/src/opencode/plugins/__tests__/version-check.test.ts +0 -223
- package/src/opencode/plugins/custom-compaction.ts +0 -1080
- package/src/opencode/plugins/file-indexer.ts +0 -516
- package/src/opencode/plugins/usethis-todo-publish.ts +0 -44
- package/src/opencode/plugins/usethis-todo-ui.ts +0 -37
- package/src/opencode/plugins/version-check.ts +0 -230
- package/src/opencode/tools/codeindex.ts +0 -264
- package/src/opencode/tools/search.ts +0 -149
- package/src/opencode/tools/usethis_todo.ts +0 -538
- package/src/vectorizer/index.js +0 -573
- package/src/vectorizer/package.json +0 -16
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TODO Tool with Dependencies & Priority — v3 (dual storage)
|
|
3
|
-
*
|
|
4
|
-
* 4 commands:
|
|
5
|
-
* usethis_todo_write({ todos: [...] }) - create/update TODO list
|
|
6
|
-
* usethis_todo_read() - read TODO with graph analysis
|
|
7
|
-
* usethis_todo_read_five() - get next 5 available tasks
|
|
8
|
-
* usethis_todo_read_by_id() - get next 5 available tasks
|
|
9
|
-
* usethis_todo_update(id, field, value) - update any task field
|
|
10
|
-
*
|
|
11
|
-
* Storage:
|
|
12
|
-
* Enhanced: .opencode/session-todos/{sid}.json (title, blockedBy, graph)
|
|
13
|
-
* Native: ~/.local/share/opencode/storage/todo/{sid}.json (TUI display)
|
|
14
|
-
*
|
|
15
|
-
* Features:
|
|
16
|
-
* - Hierarchical IDs: E01-S01-T01
|
|
17
|
-
* - Dependencies: blockedBy field
|
|
18
|
-
* - Priority: CRIT | HIGH | MED | LOW (auto-sorted)
|
|
19
|
-
* - Graph: shows available, blocked, parallel tasks
|
|
20
|
-
* - Dual write: native OpenCode storage for TUI integration
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { tool } from "@opencode-ai/plugin"
|
|
24
|
-
import path from "path"
|
|
25
|
-
import os from "os"
|
|
26
|
-
import fs from "fs/promises"
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// Types
|
|
30
|
-
// ============================================================================
|
|
31
|
-
|
|
32
|
-
interface Todo {
|
|
33
|
-
id: string // E01-S01-T01
|
|
34
|
-
content: string // Short task summary
|
|
35
|
-
description?: string // Full task description (optional)
|
|
36
|
-
releases?: string[] // Release identifiers (optional)
|
|
37
|
-
status: string // todo | in_progress | ready | done
|
|
38
|
-
priority: string // CRIT | HIGH | MED | LOW
|
|
39
|
-
blockedBy?: string[] // IDs of blocking tasks
|
|
40
|
-
createdAt?: number
|
|
41
|
-
updatedAt?: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface NativeTodo {
|
|
45
|
-
id: string
|
|
46
|
-
content: string // "title: content" combined
|
|
47
|
-
status: string // pending | in_progress | completed | cancelled
|
|
48
|
-
priority: string // high | medium | low
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface TodoGraph {
|
|
52
|
-
todos: Todo[]
|
|
53
|
-
available: string[]
|
|
54
|
-
parallel: string[][]
|
|
55
|
-
blocked: Record<string, string[]>
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ============================================================================
|
|
59
|
-
// Storage — dual write
|
|
60
|
-
// ============================================================================
|
|
61
|
-
|
|
62
|
-
// Resolve project directory (context.directory may be undefined via MCP)
|
|
63
|
-
function dir(directory?: string): string {
|
|
64
|
-
return directory || process.env.OPENCODE_PROJECT_DIR || process.cwd()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Enhanced storage path (project-local)
|
|
68
|
-
function getEnhancedPath(sid: string, directory?: string): string {
|
|
69
|
-
return path.join(dir(directory), ".opencode", "session-todos", `${sid || "current"}.json`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function getNativeDataDirs(): Promise<string[]> {
|
|
73
|
-
const dirs = new Set<string>()
|
|
74
|
-
|
|
75
|
-
// 1) xdg-basedir (what OpenCode itself uses)
|
|
76
|
-
try {
|
|
77
|
-
const mod: any = await import("xdg-basedir")
|
|
78
|
-
if (mod?.xdgData && typeof mod.xdgData === "string") {
|
|
79
|
-
dirs.add(mod.xdgData)
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
// ignore
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 2) explicit XDG override
|
|
86
|
-
if (process.env.XDG_DATA_HOME) {
|
|
87
|
-
dirs.add(process.env.XDG_DATA_HOME)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 3) common fallbacks
|
|
91
|
-
dirs.add(path.join(os.homedir(), ".local", "share"))
|
|
92
|
-
dirs.add(path.join(os.homedir(), "Library", "Application Support"))
|
|
93
|
-
|
|
94
|
-
return [...dirs]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function getNativePaths(sid: string): Promise<string[]> {
|
|
98
|
-
const baseDirs = await getNativeDataDirs()
|
|
99
|
-
const file = `${sid || "current"}.json`
|
|
100
|
-
return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Map our format → native format
|
|
104
|
-
function toNative(todo: Todo): NativeTodo {
|
|
105
|
-
// Status mapping: our → native
|
|
106
|
-
const statusMap: Record<string, string> = {
|
|
107
|
-
todo: "pending",
|
|
108
|
-
in_progress: "in_progress",
|
|
109
|
-
ready: "in_progress", // native has no "ready"
|
|
110
|
-
finished: "in_progress", // back-compat
|
|
111
|
-
done: "completed", // native uses "completed" not "done"
|
|
112
|
-
cancelled: "cancelled",
|
|
113
|
-
}
|
|
114
|
-
// Priority mapping: CRIT/HIGH/MED/LOW → high/medium/low
|
|
115
|
-
const prioMap: Record<string, string> = {
|
|
116
|
-
CRIT: "high",
|
|
117
|
-
HIGH: "high",
|
|
118
|
-
MED: "medium",
|
|
119
|
-
LOW: "low",
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const deps = todo.blockedBy?.length ? ` [← ${todo.blockedBy.join(", ")}]` : ""
|
|
123
|
-
const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
|
|
124
|
-
const rel = todo.releases?.length ? ` [rel: ${todo.releases.join(", ")}]` : ""
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
id: todo.id,
|
|
128
|
-
content: `${todo.content}${desc}${rel}${deps}`,
|
|
129
|
-
status: statusMap[todo.status] || "pending",
|
|
130
|
-
priority: prioMap[todo.priority] || "medium",
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
|
|
135
|
-
try {
|
|
136
|
-
const raw = JSON.parse(await fs.readFile(getEnhancedPath(sid, directory), "utf-8"))
|
|
137
|
-
if (!Array.isArray(raw)) return []
|
|
138
|
-
return raw.map((t: any) => normalizeTodo(t))
|
|
139
|
-
} catch {
|
|
140
|
-
return []
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function writeTodos(todos: Todo[], sid: string, directory?: string): Promise<void> {
|
|
145
|
-
// 1. Enhanced storage (our full format)
|
|
146
|
-
const enhancedPath = getEnhancedPath(sid, directory)
|
|
147
|
-
await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
|
|
148
|
-
await fs.writeFile(enhancedPath, JSON.stringify(todos, null, 2), "utf-8")
|
|
149
|
-
|
|
150
|
-
// 2. Native storage (for TUI display)
|
|
151
|
-
const nativeTodos = todos.map(toNative)
|
|
152
|
-
try {
|
|
153
|
-
const nativePaths = await getNativePaths(sid)
|
|
154
|
-
await Promise.allSettled(
|
|
155
|
-
nativePaths.map(async (nativePath) => {
|
|
156
|
-
await fs.mkdir(path.dirname(nativePath), { recursive: true })
|
|
157
|
-
await fs.writeFile(nativePath, JSON.stringify(nativeTodos, null, 2), "utf-8")
|
|
158
|
-
}),
|
|
159
|
-
)
|
|
160
|
-
} catch {
|
|
161
|
-
// Native write failure is non-fatal
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ============================================================================
|
|
166
|
-
// Graph analysis
|
|
167
|
-
// ============================================================================
|
|
168
|
-
|
|
169
|
-
function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
170
|
-
const blocked: Record<string, string[]> = {}
|
|
171
|
-
const availableTodos: Todo[] = []
|
|
172
|
-
|
|
173
|
-
for (const todo of todos) {
|
|
174
|
-
if (normalizeStatus(todo.status) !== "todo") continue
|
|
175
|
-
const activeBlockers = (todo.blockedBy || []).filter(id => {
|
|
176
|
-
const b = todos.find(t => t.id === id)
|
|
177
|
-
const bs = normalizeStatus(b?.status)
|
|
178
|
-
return b && bs !== "done" && bs !== "cancelled"
|
|
179
|
-
})
|
|
180
|
-
if (activeBlockers.length === 0) {
|
|
181
|
-
availableTodos.push(todo)
|
|
182
|
-
} else {
|
|
183
|
-
blocked[todo.id] = activeBlockers
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const P: Record<string, number> = { CRIT: 0, HIGH: 1, MED: 2, LOW: 3 }
|
|
188
|
-
availableTodos.sort((a, b) => (P[a.priority] ?? 2) - (P[b.priority] ?? 2))
|
|
189
|
-
const available = availableTodos.map(t => t.id)
|
|
190
|
-
|
|
191
|
-
// Parallel groups
|
|
192
|
-
const parallel: string[][] = []
|
|
193
|
-
const seen = new Set<string>()
|
|
194
|
-
for (const id of available) {
|
|
195
|
-
if (seen.has(id)) continue
|
|
196
|
-
const group = [id]
|
|
197
|
-
seen.add(id)
|
|
198
|
-
for (const other of available) {
|
|
199
|
-
if (seen.has(other)) continue
|
|
200
|
-
const a = todos.find(t => t.id === id)
|
|
201
|
-
const b = todos.find(t => t.id === other)
|
|
202
|
-
if (!b?.blockedBy?.includes(id) && !a?.blockedBy?.includes(other)) {
|
|
203
|
-
group.push(other)
|
|
204
|
-
seen.add(other)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (group.length > 0) parallel.push(group)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return { todos, available, parallel, blocked }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ============================================================================
|
|
214
|
-
// Formatting
|
|
215
|
-
// ============================================================================
|
|
216
|
-
|
|
217
|
-
const PE = (p?: string) => p === "CRIT" ? "🔴" : p === "HIGH" ? "🟠" : p === "LOW" ? "🟢" : "🟡"
|
|
218
|
-
const SI = (s: string) => s === "done" ? "✓" : s === "in_progress" ? "⚙" : s === "ready" ? "⏳" : s === "cancelled" ? "✗" : s === "todo" ? "○" : "·"
|
|
219
|
-
|
|
220
|
-
function normalizeStatus(input: unknown): string {
|
|
221
|
-
const s = String(input || "").trim()
|
|
222
|
-
|
|
223
|
-
// New canonical set
|
|
224
|
-
if (s === "todo" || s === "in_progress" || s === "ready" || s === "done") return s
|
|
225
|
-
|
|
226
|
-
// Back-compat (older versions)
|
|
227
|
-
if (s === "pending") return "todo"
|
|
228
|
-
if (s === "waiting_review" || s === "finished") return "ready"
|
|
229
|
-
if (s === "completed") return "done"
|
|
230
|
-
|
|
231
|
-
// Keep cancelled if it appears (native supports it)
|
|
232
|
-
if (s === "cancelled") return "cancelled"
|
|
233
|
-
|
|
234
|
-
// Default
|
|
235
|
-
return "todo"
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function normalizeReleases(input: unknown): string[] | undefined {
|
|
239
|
-
if (!Array.isArray(input)) return undefined
|
|
240
|
-
const values = input
|
|
241
|
-
.map((x) => String(x || "").trim())
|
|
242
|
-
.filter(Boolean)
|
|
243
|
-
return values.length ? values : undefined
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function normalizeTodo(input: any): Todo {
|
|
247
|
-
const status = normalizeStatus(input?.status)
|
|
248
|
-
const releases = normalizeReleases(input?.releases)
|
|
249
|
-
|
|
250
|
-
// Auto transition: ready -> done when releases exist
|
|
251
|
-
const promotedStatus = status === "ready" && releases?.length ? "done" : status
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
...input,
|
|
255
|
-
status: promotedStatus,
|
|
256
|
-
releases,
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function prioRank(p?: string): number {
|
|
261
|
-
return p === "CRIT" ? 0 : p === "HIGH" ? 1 : p === "MED" ? 2 : 3
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function sortTodosForList(todos: Todo[]): Todo[] {
|
|
265
|
-
return todos
|
|
266
|
-
.slice()
|
|
267
|
-
.sort((a, b) => (prioRank(a.priority) - prioRank(b.priority)) || a.id.localeCompare(b.id))
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function isBlocked(todo: Todo, byId: Map<string, Todo>): boolean {
|
|
271
|
-
const s = normalizeStatus(todo.status)
|
|
272
|
-
if (s !== "todo") return false
|
|
273
|
-
if (!todo.blockedBy?.length) return false
|
|
274
|
-
for (const id of todo.blockedBy) {
|
|
275
|
-
const b = byId.get(id)
|
|
276
|
-
const bs = normalizeStatus(b?.status)
|
|
277
|
-
if (!b) return true
|
|
278
|
-
if (bs !== "done" && bs !== "cancelled") return true
|
|
279
|
-
}
|
|
280
|
-
return false
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function todoLine(todo: Todo, byId: Map<string, Todo>): string {
|
|
284
|
-
const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
|
|
285
|
-
const rel = todo.releases?.length ? ` [rel: ${todo.releases.join(", ")}]` : ""
|
|
286
|
-
const deps = todo.blockedBy?.length ? ` ← ${todo.blockedBy.join(", ")}` : ""
|
|
287
|
-
const ns = normalizeStatus(todo.status)
|
|
288
|
-
const icon = isBlocked(todo, byId) ? "⊗" : SI(ns)
|
|
289
|
-
return `${icon} ${PE(todo.priority)} ${todo.id}: ${todo.content}${desc}${rel}${deps}`
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function renderNestedTodoList(todos: Todo[], allTodos?: Todo[]): string {
|
|
293
|
-
const byId = new Map((allTodos || todos).map(t => [t.id, t]))
|
|
294
|
-
|
|
295
|
-
// Group by id pattern: E01-S01-T01 → 3 nested levels (E01 → S01 → tasks)
|
|
296
|
-
const groups = new Map<string, Map<string, Todo[]>>()
|
|
297
|
-
const flat: Todo[] = []
|
|
298
|
-
|
|
299
|
-
for (const t of todos) {
|
|
300
|
-
const parts = t.id.split("-")
|
|
301
|
-
if (parts.length >= 3) {
|
|
302
|
-
const epic = parts[0]
|
|
303
|
-
const story = parts[1]
|
|
304
|
-
if (!groups.has(epic)) groups.set(epic, new Map())
|
|
305
|
-
const storyMap = groups.get(epic)!
|
|
306
|
-
if (!storyMap.has(story)) storyMap.set(story, [])
|
|
307
|
-
storyMap.get(story)!.push(t)
|
|
308
|
-
} else {
|
|
309
|
-
flat.push(t)
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const lines: string[] = []
|
|
314
|
-
const epicKeys = [...groups.keys()].sort()
|
|
315
|
-
for (const epic of epicKeys) {
|
|
316
|
-
lines.push(`- ${epic}`)
|
|
317
|
-
const storyMap = groups.get(epic)!
|
|
318
|
-
const storyKeys = [...storyMap.keys()].sort()
|
|
319
|
-
for (const story of storyKeys) {
|
|
320
|
-
lines.push(` - ${epic}-${story}`)
|
|
321
|
-
const tasks = storyMap.get(story)!
|
|
322
|
-
.slice()
|
|
323
|
-
.sort((a, b) => a.id.localeCompare(b.id))
|
|
324
|
-
for (const t of tasks) {
|
|
325
|
-
lines.push(` - ${todoLine(t, byId)}`)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const flatSorted = flat.slice().sort((a, b) => a.id.localeCompare(b.id))
|
|
331
|
-
for (const t of flatSorted) {
|
|
332
|
-
lines.push(`- ${todoLine(t, byId)}`)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return lines.length ? lines.join("\n") : "- (empty)"
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function resolveBlockers(todos: Todo[], rootIds: string[]): { blockers: Todo[]; missing: string[] } {
|
|
339
|
-
const byId = new Map(todos.map(t => [t.id, t]))
|
|
340
|
-
const blockers: Todo[] = []
|
|
341
|
-
const missing: string[] = []
|
|
342
|
-
const seen = new Set<string>()
|
|
343
|
-
const stack: string[] = []
|
|
344
|
-
|
|
345
|
-
for (const id of rootIds) {
|
|
346
|
-
const t = byId.get(id)
|
|
347
|
-
if (!t?.blockedBy?.length) continue
|
|
348
|
-
stack.push(...t.blockedBy)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
while (stack.length > 0) {
|
|
352
|
-
const id = stack.shift()!
|
|
353
|
-
if (seen.has(id)) continue
|
|
354
|
-
seen.add(id)
|
|
355
|
-
|
|
356
|
-
const t = byId.get(id)
|
|
357
|
-
if (!t) {
|
|
358
|
-
missing.push(id)
|
|
359
|
-
continue
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
blockers.push(t)
|
|
363
|
-
if (t.blockedBy?.length) stack.push(...t.blockedBy)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
blockers.sort((a, b) => a.id.localeCompare(b.id))
|
|
367
|
-
missing.sort((a, b) => a.localeCompare(b))
|
|
368
|
-
return { blockers, missing }
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function formatGraph(graph: TodoGraph): string {
|
|
372
|
-
const { todos } = graph
|
|
373
|
-
const total = todos.length
|
|
374
|
-
const done = todos.filter(t => normalizeStatus(t.status) === "done").length
|
|
375
|
-
const wip = todos.filter(t => normalizeStatus(t.status) === "in_progress").length
|
|
376
|
-
|
|
377
|
-
const availableTodos = graph.available
|
|
378
|
-
.map((id) => todos.find((t) => t.id === id))
|
|
379
|
-
.filter(Boolean) as Todo[]
|
|
380
|
-
|
|
381
|
-
const blockedTodos = Object.keys(graph.blocked)
|
|
382
|
-
.map((id) => todos.find((t) => t.id === id))
|
|
383
|
-
.filter(Boolean) as Todo[]
|
|
384
|
-
|
|
385
|
-
const lines: string[] = []
|
|
386
|
-
lines.push(`TODO Graph [${done}/${total} done, ${wip} in progress]`)
|
|
387
|
-
lines.push("")
|
|
388
|
-
lines.push("All Tasks:")
|
|
389
|
-
lines.push(renderNestedTodoList(sortTodosForList(todos), todos))
|
|
390
|
-
lines.push("")
|
|
391
|
-
lines.push("Available Now:")
|
|
392
|
-
lines.push(availableTodos.length ? renderNestedTodoList(availableTodos, todos) : "- (none)")
|
|
393
|
-
lines.push("")
|
|
394
|
-
lines.push("Blocked:")
|
|
395
|
-
lines.push(blockedTodos.length ? renderNestedTodoList(blockedTodos, todos) : "- (none)")
|
|
396
|
-
return lines.join("\n")
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// ============================================================================
|
|
400
|
-
// Tools
|
|
401
|
-
// ============================================================================
|
|
402
|
-
|
|
403
|
-
export const write = tool({
|
|
404
|
-
description: "Create or update TODO list. TODOv2 (Prefer this instead of TODO)",
|
|
405
|
-
args: {
|
|
406
|
-
todos: tool.schema.array(
|
|
407
|
-
tool.schema.object({
|
|
408
|
-
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
|
|
409
|
-
content: tool.schema.string().describe("Short task summary"),
|
|
410
|
-
description: tool.schema.string().optional().describe("Full task description"),
|
|
411
|
-
releases: tool.schema.array(tool.schema.string()).optional().describe("Release identifiers"),
|
|
412
|
-
status: tool.schema.string().describe("todo | in_progress | ready | done"),
|
|
413
|
-
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
414
|
-
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
|
|
415
|
-
})
|
|
416
|
-
).describe("Array of todos"),
|
|
417
|
-
},
|
|
418
|
-
async execute(args, context) {
|
|
419
|
-
const now = Date.now()
|
|
420
|
-
const todos = args.todos.map((t: any) => normalizeTodo({ ...t, createdAt: t.createdAt || now, updatedAt: now }))
|
|
421
|
-
await writeTodos(todos, context.sessionID, context.directory)
|
|
422
|
-
return formatGraph(analyzeGraph(todos))
|
|
423
|
-
},
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
export const read_five = tool({
|
|
427
|
-
description: "Read current TODO list. Shows Next 5 tasks.",
|
|
428
|
-
args: {},
|
|
429
|
-
async execute(_args, context) {
|
|
430
|
-
const todos = await readTodos(context.sessionID, context.directory)
|
|
431
|
-
const ready = sortTodosForList(todos.filter(t => normalizeStatus(t.status) === "todo"))
|
|
432
|
-
if (ready.length === 0) return "No tasks in todo."
|
|
433
|
-
|
|
434
|
-
const items = ready.slice(0, 5)
|
|
435
|
-
|
|
436
|
-
const rootIds = items.map(t => t.id)
|
|
437
|
-
const rootSet = new Set(rootIds)
|
|
438
|
-
const resolved = resolveBlockers(todos, rootIds)
|
|
439
|
-
const blockers = resolved.blockers.filter(t => !rootSet.has(t.id))
|
|
440
|
-
const missing = resolved.missing
|
|
441
|
-
|
|
442
|
-
const more = ready.length > 5 ? `+${ready.length - 5} more` : ""
|
|
443
|
-
|
|
444
|
-
const lines: string[] = []
|
|
445
|
-
lines.push("Next 5:")
|
|
446
|
-
lines.push(renderNestedTodoList(items, todos))
|
|
447
|
-
if (more) lines.push(more)
|
|
448
|
-
|
|
449
|
-
lines.push("")
|
|
450
|
-
lines.push(`Blocked By (resolved) [${blockers.length}]:`)
|
|
451
|
-
lines.push(blockers.length ? renderNestedTodoList(blockers, todos) : "- (none)")
|
|
452
|
-
if (missing.length) {
|
|
453
|
-
lines.push("")
|
|
454
|
-
lines.push(`Blocked By missing: ${missing.join(", ")}`)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return lines.join("\n")
|
|
458
|
-
},
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
export const read = tool({
|
|
462
|
-
description: "Read current TODO list. Shows all tasks.",
|
|
463
|
-
args: {},
|
|
464
|
-
async execute(_args, context) {
|
|
465
|
-
const todos = await readTodos(context.sessionID, context.directory)
|
|
466
|
-
if (todos.length === 0) return "No todos. Use usethis_todo_write to create."
|
|
467
|
-
return formatGraph(analyzeGraph(todos))
|
|
468
|
-
},
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
export const read_by_id = tool({
|
|
472
|
-
description: "Read task by id.",
|
|
473
|
-
args: {
|
|
474
|
-
id: tool.schema.string().describe("Task ID"),
|
|
475
|
-
},
|
|
476
|
-
async execute(args, context) {
|
|
477
|
-
const todos = await readTodos(context.sessionID, context.directory)
|
|
478
|
-
const todo = todos.find(t => t.id === args.id)
|
|
479
|
-
if (!todo) return `❌ Task ${args.id} not found`
|
|
480
|
-
|
|
481
|
-
const { blockers, missing } = resolveBlockers(todos, [todo.id])
|
|
482
|
-
|
|
483
|
-
const lines: string[] = []
|
|
484
|
-
lines.push("Task:")
|
|
485
|
-
lines.push(`- ${todoLine(todo, new Map(todos.map(t => [t.id, t])) )}`)
|
|
486
|
-
|
|
487
|
-
lines.push("")
|
|
488
|
-
lines.push("Blocked By (resolved):")
|
|
489
|
-
lines.push(blockers.length ? renderNestedTodoList(blockers, todos) : "- (none)")
|
|
490
|
-
|
|
491
|
-
if (missing.length) {
|
|
492
|
-
lines.push("")
|
|
493
|
-
lines.push(`Blocked By missing: ${missing.join(", ")}`)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return lines.join("\n")
|
|
497
|
-
},
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
export const update = tool({
|
|
501
|
-
description: "Update task(s). Send 1 or many for update",
|
|
502
|
-
args: {
|
|
503
|
-
todos: tool.schema.array(
|
|
504
|
-
tool.schema.object({
|
|
505
|
-
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
|
|
506
|
-
content: tool.schema.string().describe("Short task summary"),
|
|
507
|
-
description: tool.schema.string().optional().describe("Full task description"),
|
|
508
|
-
releases: tool.schema.array(tool.schema.string()).optional().describe("Release identifiers(from ready -> done)"),
|
|
509
|
-
status: tool.schema.string().describe("todo | in_progress | ready | done"),
|
|
510
|
-
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
511
|
-
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks(from todo -> blocked)"),
|
|
512
|
-
})
|
|
513
|
-
).describe("Array of todos to update"),
|
|
514
|
-
},
|
|
515
|
-
async execute(args, context) {
|
|
516
|
-
const todos = await readTodos(context.sessionID, context.directory)
|
|
517
|
-
const now = Date.now()
|
|
518
|
-
const byId = new Map(todos.map(t => [t.id, t]))
|
|
519
|
-
|
|
520
|
-
for (const incoming of args.todos) {
|
|
521
|
-
const normalizedIncoming: any = normalizeTodo(incoming)
|
|
522
|
-
const existing = byId.get(normalizedIncoming.id)
|
|
523
|
-
if (existing) {
|
|
524
|
-
Object.assign(existing, normalizedIncoming)
|
|
525
|
-
existing.updatedAt = now
|
|
526
|
-
// Ensure auto transition is applied after merge
|
|
527
|
-
existing.status = normalizeTodo(existing).status
|
|
528
|
-
existing.releases = normalizeTodo(existing).releases
|
|
529
|
-
} else {
|
|
530
|
-
byId.set(normalizedIncoming.id, normalizeTodo({ ...normalizedIncoming, createdAt: now, updatedAt: now }))
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const merged = [...byId.values()]
|
|
535
|
-
await writeTodos(merged, context.sessionID, context.directory)
|
|
536
|
-
return `✅ Updated ${args.todos.length} task(s)\n\n${formatGraph(analyzeGraph(merged))}`
|
|
537
|
-
},
|
|
538
|
-
})
|