@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/workflow",
3
- "version": "4.38.3-dev.0",
3
+ "version": "4.38.3-dev.1",
4
4
  "description": "Initialize OpenCode Workflow system for AI-assisted development with semantic code search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "4.38.3-dev.0",
3
- "buildDate": "2026-01-28T10:28:17.932Z",
2
+ "version": "4.38.3-dev.1",
3
+ "buildDate": "2026-01-28T11:01:04.180Z",
4
4
  "files": [
5
5
  ".gitignore",
6
6
  "config.yaml",
@@ -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, get_by_id } from "../../tools/usethis_todo"
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("get_by_id returns single task", async () => {
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 get_by_id.execute({ id: "A1" }, ctx)
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("get_by_id returns not found", async () => {
174
+ it("read_by_id returns not found", async () => {
134
175
  const ctx = { sessionID: "sess-miss", directory: tempDir } as any
135
- const out = await get_by_id.execute({ id: "NOPE" }, ctx)
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 (input.tool !== "usethis_todo_write" && input.tool !== "usethis_todo_update") return
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
- `## TODO`,
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
- * usethis_todo_read_next_five() - get next 5 available tasks
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
- nativePaths.map(async (nativePath) => {
151
- await fs.mkdir(path.dirname(nativePath), { recursive: true })
152
- await fs.writeFile(nativePath, JSON.stringify(nativeTodos, null, 2), "utf-8")
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
- tool.schema.object({
270
- id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
271
- content: tool.schema.string().describe("Short task summary"),
272
- description: tool.schema.string().optional().describe("Full task description"),
273
- status: tool.schema.string().describe("pending | ready | in_progress | waiting_review | done | cancelled"),
274
- priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
275
- blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
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 read_next_five = tool({
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 get_by_id = tool({
320
- description: "Get one task by id.",
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 deps = todo.blockedBy?.length ? `\nblockedBy: ${todo.blockedBy.join(", ")}` : ""
330
- const desc = todo.description?.trim() ? `\n\ndescription:\n${todo.description.trim()}` : ""
331
- return [
332
- `id: ${todo.id}`,
333
- `priority: ${todo.priority}`,
334
- `status: ${todo.status}`,
335
- deps,
336
- `\ncontent:\n${todo.content}`,
337
- desc,
338
- ].filter(Boolean).join("\n")
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 tasks. Same interface as write, but merges by id.",
391
+ description: "Update task(s). Send 1 or many for update",
344
392
  args: {
345
393
  todos: tool.schema.array(
346
- tool.schema.object({
347
- id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
348
- content: tool.schema.string().describe("Short task summary"),
349
- description: tool.schema.string().optional().describe("Full task description"),
350
- status: tool.schema.string().describe("pending | ready | in_progress | waiting_review | done | cancelled"),
351
- priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
352
- blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
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) {