@comfanion/workflow 4.38.3-dev.0 → 4.38.3-dev.1
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
CHANGED
package/src/build-info.json
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
2
2
|
import { readFile } from "fs/promises"
|
|
3
3
|
import { join } from "path"
|
|
4
4
|
import { createTempDir, cleanupTempDir } from "./helpers/mock-ctx"
|
|
5
|
-
import { write, update, read,
|
|
5
|
+
import { write, update, read, read_by_id, read_five } from "../../tools/usethis_todo"
|
|
6
6
|
|
|
7
7
|
describe("usethis_todo tool", () => {
|
|
8
8
|
let tempDir: string
|
|
@@ -103,7 +103,31 @@ describe("usethis_todo tool", () => {
|
|
|
103
103
|
expect(output).toContain("Available Now")
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
-
it("
|
|
106
|
+
it("read_five returns up to 5 available tasks with description", async () => {
|
|
107
|
+
const ctx = { sessionID: "sess-five", directory: tempDir } as any
|
|
108
|
+
await write.execute(
|
|
109
|
+
{
|
|
110
|
+
todos: [
|
|
111
|
+
{ id: "A1", content: "T1", description: "D1", status: "ready", priority: "HIGH" },
|
|
112
|
+
{ id: "A2", content: "T2", status: "ready", priority: "MED" },
|
|
113
|
+
{ id: "A3", content: "T3", status: "ready", priority: "LOW" },
|
|
114
|
+
{ id: "A4", content: "T4", status: "ready", priority: "LOW" },
|
|
115
|
+
{ id: "A5", content: "T5", status: "ready", priority: "LOW" },
|
|
116
|
+
{ id: "A6", content: "T6", status: "ready", priority: "LOW" },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
ctx
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const out = await read_five.execute({}, ctx)
|
|
123
|
+
expect(out).toContain("Next 5")
|
|
124
|
+
expect(out).toContain("A1")
|
|
125
|
+
expect(out).toContain("T1")
|
|
126
|
+
expect(out).toContain("D1")
|
|
127
|
+
expect(out).toContain("+1 more")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("read_by_id returns task with resolved blockers", async () => {
|
|
107
131
|
const ctx = { sessionID: "sess-get", directory: tempDir } as any
|
|
108
132
|
await write.execute(
|
|
109
133
|
{
|
|
@@ -116,23 +140,40 @@ describe("usethis_todo tool", () => {
|
|
|
116
140
|
priority: "HIGH",
|
|
117
141
|
blockedBy: ["B1"],
|
|
118
142
|
},
|
|
143
|
+
{
|
|
144
|
+
id: "B1",
|
|
145
|
+
content: "Blocker task",
|
|
146
|
+
status: "done",
|
|
147
|
+
priority: "LOW",
|
|
148
|
+
blockedBy: ["C1"],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "C1",
|
|
152
|
+
content: "Root blocker",
|
|
153
|
+
status: "pending",
|
|
154
|
+
priority: "MED",
|
|
155
|
+
},
|
|
119
156
|
],
|
|
120
157
|
},
|
|
121
158
|
ctx
|
|
122
159
|
)
|
|
123
160
|
|
|
124
|
-
const out = await
|
|
161
|
+
const out = await read_by_id.execute({ id: "A1" }, ctx)
|
|
125
162
|
expect(out).toContain("id: A1")
|
|
126
163
|
expect(out).toContain("content:")
|
|
127
164
|
expect(out).toContain("Task content")
|
|
128
165
|
expect(out).toContain("blockedBy: B1")
|
|
129
166
|
expect(out).toContain("description:")
|
|
130
167
|
expect(out).toContain("More details")
|
|
168
|
+
|
|
169
|
+
expect(out).toContain("blockedBy (resolved):")
|
|
170
|
+
expect(out).toContain("- B1")
|
|
171
|
+
expect(out).toContain("- C1")
|
|
131
172
|
})
|
|
132
173
|
|
|
133
|
-
it("
|
|
174
|
+
it("read_by_id returns not found", async () => {
|
|
134
175
|
const ctx = { sessionID: "sess-miss", directory: tempDir } as any
|
|
135
|
-
const out = await
|
|
176
|
+
const out = await read_by_id.execute({ id: "NOPE" }, ctx)
|
|
136
177
|
expect(out).toContain("not found")
|
|
137
178
|
})
|
|
138
179
|
})
|
|
@@ -9,10 +9,16 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
9
9
|
export const UsethisTodoPublish: Plugin = async ({ client }) => {
|
|
10
10
|
return {
|
|
11
11
|
"tool.execute.after": async (input, output) => {
|
|
12
|
-
if (
|
|
12
|
+
if (
|
|
13
|
+
input.tool !== "usethis_todo_write"
|
|
14
|
+
&& input.tool !== "usethis_todo_update"
|
|
15
|
+
&& input.tool !== "usethis_todo_read_five"
|
|
16
|
+
&& input.tool !== "usethis_todo_read"
|
|
17
|
+
&& input.tool !== "usethis_todo_read_by_id"
|
|
18
|
+
) return
|
|
13
19
|
|
|
14
20
|
const text = [
|
|
15
|
-
|
|
21
|
+
`## TODO`,
|
|
16
22
|
// `session: ${input.sessionID}`,
|
|
17
23
|
"",
|
|
18
24
|
output.output
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TODO Tool with Dependencies & Priority — v3 (dual storage)
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* 4 commands:
|
|
5
5
|
* usethis_todo_write({ todos: [...] }) - create/update TODO list
|
|
6
6
|
* usethis_todo_read() - read TODO with graph analysis
|
|
7
|
-
*
|
|
7
|
+
* usethis_todo_read_five() - get next 5 available tasks
|
|
8
|
+
* usethis_todo_read_by_id() - get next 5 available tasks
|
|
8
9
|
* usethis_todo_update(id, field, value) - update any task field
|
|
9
|
-
*
|
|
10
|
+
*
|
|
10
11
|
* Storage:
|
|
11
12
|
* Enhanced: .opencode/session-todos/{sid}.json (title, blockedBy, graph)
|
|
12
13
|
* Native: ~/.local/share/opencode/storage/todo/{sid}.json (TUI display)
|
|
13
|
-
*
|
|
14
|
+
*
|
|
14
15
|
* Features:
|
|
15
16
|
* - Hierarchical IDs: E01-S01-T01
|
|
16
17
|
* - Dependencies: blockedBy field
|
|
@@ -116,7 +117,7 @@ function toNative(todo: Todo): NativeTodo {
|
|
|
116
117
|
MED: "medium",
|
|
117
118
|
LOW: "low",
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
|
|
120
121
|
const deps = todo.blockedBy?.length ? ` [← ${todo.blockedBy.join(", ")}]` : ""
|
|
121
122
|
const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
|
|
122
123
|
|
|
@@ -141,16 +142,16 @@ async function writeTodos(todos: Todo[], sid: string, directory?: string): Promi
|
|
|
141
142
|
const enhancedPath = getEnhancedPath(sid, directory)
|
|
142
143
|
await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
|
|
143
144
|
await fs.writeFile(enhancedPath, JSON.stringify(todos, null, 2), "utf-8")
|
|
144
|
-
|
|
145
|
+
|
|
145
146
|
// 2. Native storage (for TUI display)
|
|
146
147
|
const nativeTodos = todos.map(toNative)
|
|
147
148
|
try {
|
|
148
149
|
const nativePaths = await getNativePaths(sid)
|
|
149
150
|
await Promise.allSettled(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
nativePaths.map(async (nativePath) => {
|
|
152
|
+
await fs.mkdir(path.dirname(nativePath), { recursive: true })
|
|
153
|
+
await fs.writeFile(nativePath, JSON.stringify(nativeTodos, null, 2), "utf-8")
|
|
154
|
+
}),
|
|
154
155
|
)
|
|
155
156
|
} catch {
|
|
156
157
|
// Native write failure is non-fatal
|
|
@@ -164,7 +165,7 @@ async function writeTodos(todos: Todo[], sid: string, directory?: string): Promi
|
|
|
164
165
|
function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
165
166
|
const blocked: Record<string, string[]> = {}
|
|
166
167
|
const availableTodos: Todo[] = []
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
for (const todo of todos) {
|
|
169
170
|
if (todo.status !== "ready") continue
|
|
170
171
|
const activeBlockers = (todo.blockedBy || []).filter(id => {
|
|
@@ -177,11 +178,11 @@ function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
|
177
178
|
blocked[todo.id] = activeBlockers
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
const P: Record<string, number> = { CRIT: 0, HIGH: 1, MED: 2, LOW: 3 }
|
|
182
183
|
availableTodos.sort((a, b) => (P[a.priority] ?? 2) - (P[b.priority] ?? 2))
|
|
183
184
|
const available = availableTodos.map(t => t.id)
|
|
184
|
-
|
|
185
|
+
|
|
185
186
|
// Parallel groups
|
|
186
187
|
const parallel: string[][] = []
|
|
187
188
|
const seen = new Set<string>()
|
|
@@ -200,7 +201,7 @@ function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
|
200
201
|
}
|
|
201
202
|
if (group.length > 0) parallel.push(group)
|
|
202
203
|
}
|
|
203
|
-
|
|
204
|
+
|
|
204
205
|
return { todos, available, parallel, blocked }
|
|
205
206
|
}
|
|
206
207
|
|
|
@@ -216,9 +217,9 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
216
217
|
const total = todos.length
|
|
217
218
|
const done = todos.filter(t => t.status === "done").length
|
|
218
219
|
const wip = todos.filter(t => t.status === "in_progress").length
|
|
219
|
-
|
|
220
|
+
|
|
220
221
|
const lines: string[] = [`═══ TODO Graph [${done}/${total} done, ${wip} in progress] ═══`, ""]
|
|
221
|
-
|
|
222
|
+
|
|
222
223
|
lines.push("All Tasks:")
|
|
223
224
|
for (const t of todos) {
|
|
224
225
|
const deps = t.blockedBy?.length ? ` ← ${t.blockedBy.join(", ")}` : ""
|
|
@@ -226,7 +227,7 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
226
227
|
lines.push(` ${SI(t.status)} ${PE(t.priority)} ${t.id}: ${t.content}${desc}${deps}`)
|
|
227
228
|
}
|
|
228
229
|
lines.push("")
|
|
229
|
-
|
|
230
|
+
|
|
230
231
|
if (graph.available.length > 0) {
|
|
231
232
|
lines.push("Available Now:")
|
|
232
233
|
for (const id of graph.available) {
|
|
@@ -238,14 +239,14 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
238
239
|
lines.push("Available Now: none")
|
|
239
240
|
}
|
|
240
241
|
lines.push("")
|
|
241
|
-
|
|
242
|
+
|
|
242
243
|
const multi = graph.parallel.filter(g => g.length > 1)
|
|
243
244
|
if (multi.length > 0) {
|
|
244
245
|
lines.push("Parallel Groups:")
|
|
245
246
|
multi.forEach((g, i) => lines.push(` Group ${i + 1}: ${g.join(", ")}`))
|
|
246
247
|
lines.push("")
|
|
247
248
|
}
|
|
248
|
-
|
|
249
|
+
|
|
249
250
|
if (Object.keys(graph.blocked).length > 0) {
|
|
250
251
|
lines.push("Blocked:")
|
|
251
252
|
for (const [id, blockers] of Object.entries(graph.blocked)) {
|
|
@@ -254,7 +255,7 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
254
255
|
lines.push(` ⊗ ${id}: ${t?.content}${desc} ← waiting: ${blockers.join(", ")}`)
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
|
-
|
|
258
|
+
|
|
258
259
|
return lines.join("\n")
|
|
259
260
|
}
|
|
260
261
|
|
|
@@ -266,14 +267,14 @@ export const write = tool({
|
|
|
266
267
|
description: "Create or update TODO list. TODOv2 (Prefer this instead of TODO)",
|
|
267
268
|
args: {
|
|
268
269
|
todos: tool.schema.array(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
270
|
+
tool.schema.object({
|
|
271
|
+
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
|
|
272
|
+
content: tool.schema.string().describe("Short task summary"),
|
|
273
|
+
description: tool.schema.string().optional().describe("Full task description"),
|
|
274
|
+
status: tool.schema.string().describe("pending | ready | in_progress | waiting_review | done | cancelled"),
|
|
275
|
+
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
276
|
+
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
|
|
277
|
+
})
|
|
277
278
|
).describe("Array of todos"),
|
|
278
279
|
},
|
|
279
280
|
async execute(args, context) {
|
|
@@ -284,7 +285,7 @@ export const write = tool({
|
|
|
284
285
|
},
|
|
285
286
|
})
|
|
286
287
|
|
|
287
|
-
export const
|
|
288
|
+
export const read_five = tool({
|
|
288
289
|
description: "Read current TODO list. Shows Next 5 tasks.",
|
|
289
290
|
args: {},
|
|
290
291
|
async execute(_args, context) {
|
|
@@ -316,8 +317,8 @@ export const read = tool({
|
|
|
316
317
|
},
|
|
317
318
|
})
|
|
318
319
|
|
|
319
|
-
export const
|
|
320
|
-
description: "
|
|
320
|
+
export const read_by_id = tool({
|
|
321
|
+
description: "Read task by id.",
|
|
321
322
|
args: {
|
|
322
323
|
id: tool.schema.string().describe("Task ID"),
|
|
323
324
|
},
|
|
@@ -326,31 +327,78 @@ export const get_by_id = tool({
|
|
|
326
327
|
const todo = todos.find(t => t.id === args.id)
|
|
327
328
|
if (!todo) return `❌ Task ${args.id} not found`
|
|
328
329
|
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
330
|
+
const byId = new Map(todos.map(t => [t.id, t]))
|
|
331
|
+
|
|
332
|
+
// Resolve blockers transitively (task -> blockedBy -> blockedBy ...)
|
|
333
|
+
const blockers: Todo[] = []
|
|
334
|
+
const missing: string[] = []
|
|
335
|
+
const seen = new Set<string>()
|
|
336
|
+
const stack = [...(todo.blockedBy || [])]
|
|
337
|
+
|
|
338
|
+
while (stack.length > 0) {
|
|
339
|
+
const id = stack.shift()!
|
|
340
|
+
if (seen.has(id)) continue
|
|
341
|
+
seen.add(id)
|
|
342
|
+
|
|
343
|
+
const t = byId.get(id)
|
|
344
|
+
if (!t) {
|
|
345
|
+
missing.push(id)
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
blockers.push(t)
|
|
350
|
+
if (t.blockedBy?.length) stack.push(...t.blockedBy)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const lines: string[] = []
|
|
354
|
+
lines.push(`id: ${todo.id}`)
|
|
355
|
+
lines.push(`priority: ${todo.priority}`)
|
|
356
|
+
lines.push(`status: ${todo.status}`)
|
|
357
|
+
|
|
358
|
+
if (todo.blockedBy?.length) {
|
|
359
|
+
lines.push(`blockedBy: ${todo.blockedBy.join(", ")}`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
lines.push("")
|
|
363
|
+
lines.push("content:")
|
|
364
|
+
lines.push(todo.content)
|
|
365
|
+
|
|
366
|
+
if (todo.description?.trim()) {
|
|
367
|
+
lines.push("")
|
|
368
|
+
lines.push("description:")
|
|
369
|
+
lines.push(todo.description.trim())
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (blockers.length) {
|
|
373
|
+
lines.push("")
|
|
374
|
+
lines.push("blockedBy (resolved):")
|
|
375
|
+
for (const b of blockers) {
|
|
376
|
+
const d = b.description?.trim() ? ` — ${b.description.trim()}` : ""
|
|
377
|
+
lines.push(`- ${b.id} [${b.status}] ${b.priority}: ${b.content}${d}`)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (missing.length) {
|
|
382
|
+
lines.push("")
|
|
383
|
+
lines.push(`blockedBy missing: ${missing.join(", ")}`)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return lines.join("\n")
|
|
339
387
|
},
|
|
340
388
|
})
|
|
341
389
|
|
|
342
390
|
export const update = tool({
|
|
343
|
-
description: "Update
|
|
391
|
+
description: "Update task(s). Send 1 or many for update",
|
|
344
392
|
args: {
|
|
345
393
|
todos: tool.schema.array(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
394
|
+
tool.schema.object({
|
|
395
|
+
id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
|
|
396
|
+
content: tool.schema.string().describe("Short task summary"),
|
|
397
|
+
description: tool.schema.string().optional().describe("Full task description"),
|
|
398
|
+
status: tool.schema.string().describe("pending | ready | in_progress | waiting_review | done | cancelled"),
|
|
399
|
+
priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
|
|
400
|
+
blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
|
|
401
|
+
})
|
|
354
402
|
).describe("Array of todos to update"),
|
|
355
403
|
},
|
|
356
404
|
async execute(args, context) {
|