@comfanion/workflow 4.38.2 → 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 +1 -1
- package/src/build-info.json +2 -2
- package/src/opencode/agents/architect.md +1 -1
- package/src/opencode/agents/dev.md +2 -18
- package/src/opencode/agents/pm.md +1 -1
- package/src/opencode/config.yaml +1 -1
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +81 -4
- package/src/opencode/plugins/usethis-todo-publish.ts +8 -2
- package/src/opencode/tools/usethis_todo.ts +123 -44
package/package.json
CHANGED
package/src/build-info.json
CHANGED
|
@@ -123,7 +123,7 @@ permission:
|
|
|
123
123
|
</size-awareness>
|
|
124
124
|
|
|
125
125
|
<phase name="2. Planning">
|
|
126
|
-
<action>Create tasklist with todowrite()</action>
|
|
126
|
+
<action>Create tasklist with todowrite() (TODOv2)</action>
|
|
127
127
|
<action>Present plan to user with specific files/changes</action>
|
|
128
128
|
<action>Ask for confirmation with question() tool</action>
|
|
129
129
|
<action>WAIT for user approval before proceeding</action>
|
|
@@ -4,7 +4,7 @@ mode: all # Can be primary agent or invoked via @dev
|
|
|
4
4
|
temperature: 0.2
|
|
5
5
|
|
|
6
6
|
model: anthropic/claude-opus-4-5 # Strong
|
|
7
|
-
#model:
|
|
7
|
+
#model: zai-coding-plan/glm-4.7 # Can break
|
|
8
8
|
#model: openai/gpt-5.2-codex
|
|
9
9
|
|
|
10
10
|
# Tools - FULL ACCESS for implementation
|
|
@@ -40,6 +40,7 @@ permission:
|
|
|
40
40
|
<step n="2">IMMEDIATE: store {user_name}, {communication_language} from .opencode/config.yaml</step>
|
|
41
41
|
<step n="3">Greet user by {user_name}, communicate in {communication_language}</step>
|
|
42
42
|
<step n="4">Understand user request and select appropriate skill</step>
|
|
43
|
+
<step n="5">Create tasklist with todowrite() (TODOv2)</step>
|
|
43
44
|
|
|
44
45
|
<search-first critical="MANDATORY - DO THIS BEFORE GLOB/GREP">
|
|
45
46
|
BEFORE using glob or grep, you MUST call search() first:
|
|
@@ -68,23 +69,6 @@ permission:
|
|
|
68
69
|
<r critical="MANDATORY">🔍 SEARCH FIRST: Call search() BEFORE glob when exploring codebase.
|
|
69
70
|
search({ query: "feature pattern", index: "code" }) → THEN glob if needed</r>
|
|
70
71
|
</rules>
|
|
71
|
-
|
|
72
|
-
<todo-usage hint="How to use TODO for tracking">
|
|
73
|
-
<create>
|
|
74
|
-
todowrite([
|
|
75
|
-
{ id: "story-task-1", content: "Task 1: Create entity", status: "pending", priority: "high" },
|
|
76
|
-
{ id: "story-task-2", content: "Task 2: Add repository", status: "pending", priority: "medium" },
|
|
77
|
-
...
|
|
78
|
-
])
|
|
79
|
-
</create>
|
|
80
|
-
<update-progress>
|
|
81
|
-
todoread() → get current list
|
|
82
|
-
todowrite([...list with task.status = "in_progress"])
|
|
83
|
-
</update-progress>
|
|
84
|
-
<mark-complete>
|
|
85
|
-
todowrite([...list with task.status = "completed"])
|
|
86
|
-
</mark-complete>
|
|
87
|
-
</todo-usage>
|
|
88
72
|
</activation>
|
|
89
73
|
|
|
90
74
|
<persona>
|
|
@@ -105,7 +105,7 @@ permission:
|
|
|
105
105
|
</phase>
|
|
106
106
|
|
|
107
107
|
<phase name="2. Planning">
|
|
108
|
-
<action>Create tasklist with todowrite()</action>
|
|
108
|
+
<action>Create tasklist with todowrite() (TODOv2)</action>
|
|
109
109
|
<action>Present plan to user with specific deliverables</action>
|
|
110
110
|
<action>Ask for confirmation with question() tool</action>
|
|
111
111
|
<action>WAIT for user approval before proceeding</action>
|
package/src/opencode/config.yaml
CHANGED
|
@@ -211,7 +211,7 @@ epic_workflow:
|
|
|
211
211
|
# Run integration tests after each story
|
|
212
212
|
# When true: story done → run integration tests → continue
|
|
213
213
|
# When false: skip per-story integration tests
|
|
214
|
-
test_after_each_story:
|
|
214
|
+
test_after_each_story: true
|
|
215
215
|
|
|
216
216
|
# Run integration tests after epic complete
|
|
217
217
|
# When true: all stories done → run epic integration tests
|
|
@@ -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 } 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
|
|
@@ -28,7 +28,7 @@ describe("usethis_todo tool", () => {
|
|
|
28
28
|
const output = await write.execute(
|
|
29
29
|
{
|
|
30
30
|
todos: [
|
|
31
|
-
{ id: "A1", content: "First task", status: "ready", priority: "HIGH" },
|
|
31
|
+
{ id: "A1", content: "First task", description: "Longer details", status: "ready", priority: "HIGH" },
|
|
32
32
|
{ id: "A2", content: "Second task", status: "pending", priority: "LOW" },
|
|
33
33
|
],
|
|
34
34
|
},
|
|
@@ -41,6 +41,7 @@ describe("usethis_todo tool", () => {
|
|
|
41
41
|
const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
|
|
42
42
|
expect(enhanced.length).toBe(2)
|
|
43
43
|
expect(enhanced[0].content).toBe("First task")
|
|
44
|
+
expect(enhanced[0].description).toBe("Longer details")
|
|
44
45
|
|
|
45
46
|
const nativePath = join(
|
|
46
47
|
process.env.XDG_DATA_HOME!,
|
|
@@ -52,6 +53,7 @@ describe("usethis_todo tool", () => {
|
|
|
52
53
|
const native = JSON.parse(await readFile(nativePath, "utf-8"))
|
|
53
54
|
expect(native.length).toBe(2)
|
|
54
55
|
expect(native[0].content).toContain("First task")
|
|
56
|
+
expect(native[0].content).toContain("Longer details")
|
|
55
57
|
})
|
|
56
58
|
|
|
57
59
|
it("update merges by id and can add new tasks", async () => {
|
|
@@ -60,7 +62,7 @@ describe("usethis_todo tool", () => {
|
|
|
60
62
|
await write.execute(
|
|
61
63
|
{
|
|
62
64
|
todos: [
|
|
63
|
-
{ id: "A1", content: "First task", status: "pending", priority: "MED" },
|
|
65
|
+
{ id: "A1", content: "First task", description: "v1", status: "pending", priority: "MED" },
|
|
64
66
|
],
|
|
65
67
|
},
|
|
66
68
|
ctx
|
|
@@ -69,7 +71,7 @@ describe("usethis_todo tool", () => {
|
|
|
69
71
|
const result = await update.execute(
|
|
70
72
|
{
|
|
71
73
|
todos: [
|
|
72
|
-
{ id: "A1", content: "First task", status: "done", priority: "MED" },
|
|
74
|
+
{ id: "A1", content: "First task", description: "v2", status: "done", priority: "MED" },
|
|
73
75
|
{ id: "A2", content: "Second task", status: "ready", priority: "LOW" },
|
|
74
76
|
],
|
|
75
77
|
},
|
|
@@ -82,6 +84,7 @@ describe("usethis_todo tool", () => {
|
|
|
82
84
|
const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
|
|
83
85
|
expect(enhanced.length).toBe(2)
|
|
84
86
|
expect(enhanced.find((t: any) => t.id === "A1")?.status).toBe("done")
|
|
87
|
+
expect(enhanced.find((t: any) => t.id === "A1")?.description).toBe("v2")
|
|
85
88
|
})
|
|
86
89
|
|
|
87
90
|
it("read returns graph with content", async () => {
|
|
@@ -99,4 +102,78 @@ describe("usethis_todo tool", () => {
|
|
|
99
102
|
expect(output).toContain("Task content")
|
|
100
103
|
expect(output).toContain("Available Now")
|
|
101
104
|
})
|
|
105
|
+
|
|
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 () => {
|
|
131
|
+
const ctx = { sessionID: "sess-get", directory: tempDir } as any
|
|
132
|
+
await write.execute(
|
|
133
|
+
{
|
|
134
|
+
todos: [
|
|
135
|
+
{
|
|
136
|
+
id: "A1",
|
|
137
|
+
content: "Task content",
|
|
138
|
+
description: "More details",
|
|
139
|
+
status: "ready",
|
|
140
|
+
priority: "HIGH",
|
|
141
|
+
blockedBy: ["B1"],
|
|
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
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
ctx
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const out = await read_by_id.execute({ id: "A1" }, ctx)
|
|
162
|
+
expect(out).toContain("id: A1")
|
|
163
|
+
expect(out).toContain("content:")
|
|
164
|
+
expect(out).toContain("Task content")
|
|
165
|
+
expect(out).toContain("blockedBy: B1")
|
|
166
|
+
expect(out).toContain("description:")
|
|
167
|
+
expect(out).toContain("More details")
|
|
168
|
+
|
|
169
|
+
expect(out).toContain("blockedBy (resolved):")
|
|
170
|
+
expect(out).toContain("- B1")
|
|
171
|
+
expect(out).toContain("- C1")
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("read_by_id returns not found", async () => {
|
|
175
|
+
const ctx = { sessionID: "sess-miss", directory: tempDir } as any
|
|
176
|
+
const out = await read_by_id.execute({ id: "NOPE" }, ctx)
|
|
177
|
+
expect(out).toContain("not found")
|
|
178
|
+
})
|
|
102
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
|
|
@@ -30,7 +31,8 @@ import fs from "fs/promises"
|
|
|
30
31
|
|
|
31
32
|
interface Todo {
|
|
32
33
|
id: string // E01-S01-T01
|
|
33
|
-
content: string //
|
|
34
|
+
content: string // Short task summary
|
|
35
|
+
description?: string // Full task description (optional)
|
|
34
36
|
status: string // pending | ready | in_progress | waiting_review | done | cancelled
|
|
35
37
|
priority: string // CRIT | HIGH | MED | LOW
|
|
36
38
|
blockedBy?: string[] // IDs of blocking tasks
|
|
@@ -115,12 +117,13 @@ function toNative(todo: Todo): NativeTodo {
|
|
|
115
117
|
MED: "medium",
|
|
116
118
|
LOW: "low",
|
|
117
119
|
}
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
const deps = todo.blockedBy?.length ? ` [← ${todo.blockedBy.join(", ")}]` : ""
|
|
122
|
+
const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
|
|
120
123
|
|
|
121
124
|
return {
|
|
122
125
|
id: todo.id,
|
|
123
|
-
content: `${todo.content}${deps}`,
|
|
126
|
+
content: `${todo.content}${desc}${deps}`,
|
|
124
127
|
status: statusMap[todo.status] || "pending",
|
|
125
128
|
priority: prioMap[todo.priority] || "medium",
|
|
126
129
|
}
|
|
@@ -139,16 +142,16 @@ async function writeTodos(todos: Todo[], sid: string, directory?: string): Promi
|
|
|
139
142
|
const enhancedPath = getEnhancedPath(sid, directory)
|
|
140
143
|
await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
|
|
141
144
|
await fs.writeFile(enhancedPath, JSON.stringify(todos, null, 2), "utf-8")
|
|
142
|
-
|
|
145
|
+
|
|
143
146
|
// 2. Native storage (for TUI display)
|
|
144
147
|
const nativeTodos = todos.map(toNative)
|
|
145
148
|
try {
|
|
146
149
|
const nativePaths = await getNativePaths(sid)
|
|
147
150
|
await Promise.allSettled(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
}),
|
|
152
155
|
)
|
|
153
156
|
} catch {
|
|
154
157
|
// Native write failure is non-fatal
|
|
@@ -162,7 +165,7 @@ async function writeTodos(todos: Todo[], sid: string, directory?: string): Promi
|
|
|
162
165
|
function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
163
166
|
const blocked: Record<string, string[]> = {}
|
|
164
167
|
const availableTodos: Todo[] = []
|
|
165
|
-
|
|
168
|
+
|
|
166
169
|
for (const todo of todos) {
|
|
167
170
|
if (todo.status !== "ready") continue
|
|
168
171
|
const activeBlockers = (todo.blockedBy || []).filter(id => {
|
|
@@ -175,11 +178,11 @@ function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
|
175
178
|
blocked[todo.id] = activeBlockers
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
|
-
|
|
181
|
+
|
|
179
182
|
const P: Record<string, number> = { CRIT: 0, HIGH: 1, MED: 2, LOW: 3 }
|
|
180
183
|
availableTodos.sort((a, b) => (P[a.priority] ?? 2) - (P[b.priority] ?? 2))
|
|
181
184
|
const available = availableTodos.map(t => t.id)
|
|
182
|
-
|
|
185
|
+
|
|
183
186
|
// Parallel groups
|
|
184
187
|
const parallel: string[][] = []
|
|
185
188
|
const seen = new Set<string>()
|
|
@@ -198,7 +201,7 @@ function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
|
198
201
|
}
|
|
199
202
|
if (group.length > 0) parallel.push(group)
|
|
200
203
|
}
|
|
201
|
-
|
|
204
|
+
|
|
202
205
|
return { todos, available, parallel, blocked }
|
|
203
206
|
}
|
|
204
207
|
|
|
@@ -214,42 +217,45 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
214
217
|
const total = todos.length
|
|
215
218
|
const done = todos.filter(t => t.status === "done").length
|
|
216
219
|
const wip = todos.filter(t => t.status === "in_progress").length
|
|
217
|
-
|
|
220
|
+
|
|
218
221
|
const lines: string[] = [`═══ TODO Graph [${done}/${total} done, ${wip} in progress] ═══`, ""]
|
|
219
|
-
|
|
222
|
+
|
|
220
223
|
lines.push("All Tasks:")
|
|
221
224
|
for (const t of todos) {
|
|
222
225
|
const deps = t.blockedBy?.length ? ` ← ${t.blockedBy.join(", ")}` : ""
|
|
223
|
-
|
|
226
|
+
const desc = t.description?.trim() ? ` — ${t.description.trim()}` : ""
|
|
227
|
+
lines.push(` ${SI(t.status)} ${PE(t.priority)} ${t.id}: ${t.content}${desc}${deps}`)
|
|
224
228
|
}
|
|
225
229
|
lines.push("")
|
|
226
|
-
|
|
230
|
+
|
|
227
231
|
if (graph.available.length > 0) {
|
|
228
232
|
lines.push("Available Now:")
|
|
229
233
|
for (const id of graph.available) {
|
|
230
234
|
const t = todos.find(x => x.id === id)
|
|
231
|
-
|
|
235
|
+
const desc = t?.description?.trim() ? ` — ${t.description.trim()}` : ""
|
|
236
|
+
lines.push(` → ${PE(t?.priority)} ${id}: ${t?.content}${desc}`)
|
|
232
237
|
}
|
|
233
238
|
} else {
|
|
234
239
|
lines.push("Available Now: none")
|
|
235
240
|
}
|
|
236
241
|
lines.push("")
|
|
237
|
-
|
|
242
|
+
|
|
238
243
|
const multi = graph.parallel.filter(g => g.length > 1)
|
|
239
244
|
if (multi.length > 0) {
|
|
240
245
|
lines.push("Parallel Groups:")
|
|
241
246
|
multi.forEach((g, i) => lines.push(` Group ${i + 1}: ${g.join(", ")}`))
|
|
242
247
|
lines.push("")
|
|
243
248
|
}
|
|
244
|
-
|
|
249
|
+
|
|
245
250
|
if (Object.keys(graph.blocked).length > 0) {
|
|
246
251
|
lines.push("Blocked:")
|
|
247
252
|
for (const [id, blockers] of Object.entries(graph.blocked)) {
|
|
248
253
|
const t = todos.find(x => x.id === id)
|
|
249
|
-
|
|
254
|
+
const desc = t?.description?.trim() ? ` — ${t.description.trim()}` : ""
|
|
255
|
+
lines.push(` ⊗ ${id}: ${t?.content}${desc} ← waiting: ${blockers.join(", ")}`)
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
|
-
|
|
258
|
+
|
|
253
259
|
return lines.join("\n")
|
|
254
260
|
}
|
|
255
261
|
|
|
@@ -258,27 +264,28 @@ function formatGraph(graph: TodoGraph): string {
|
|
|
258
264
|
// ============================================================================
|
|
259
265
|
|
|
260
266
|
export const write = tool({
|
|
261
|
-
description: "Create or update TODO list. TODOv2",
|
|
267
|
+
description: "Create or update TODO list. TODOv2 (Prefer this instead of TODO)",
|
|
262
268
|
args: {
|
|
263
269
|
todos: tool.schema.array(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
})
|
|
271
278
|
).describe("Array of todos"),
|
|
272
279
|
},
|
|
273
280
|
async execute(args, context) {
|
|
274
281
|
const now = Date.now()
|
|
275
|
-
const todos = args.todos.map(t => ({ ...t, createdAt: t.createdAt || now, updatedAt: now }))
|
|
282
|
+
const todos = args.todos.map((t: any) => ({ ...t, createdAt: t.createdAt || now, updatedAt: now }))
|
|
276
283
|
await writeTodos(todos, context.sessionID, context.directory)
|
|
277
284
|
return formatGraph(analyzeGraph(todos))
|
|
278
285
|
},
|
|
279
286
|
})
|
|
280
287
|
|
|
281
|
-
export const
|
|
288
|
+
export const read_five = tool({
|
|
282
289
|
description: "Read current TODO list. Shows Next 5 tasks.",
|
|
283
290
|
args: {},
|
|
284
291
|
async execute(_args, context) {
|
|
@@ -290,7 +297,8 @@ export const read_next_five = tool({
|
|
|
290
297
|
for (const id of next5) {
|
|
291
298
|
const t = graph.todos.find(x => x.id === id)
|
|
292
299
|
if (t) {
|
|
293
|
-
|
|
300
|
+
const desc = t.description?.trim() ? `\n ${t.description.trim()}` : ""
|
|
301
|
+
lines.push(`${PE(t.priority)} ${id}: ${t.content}${desc}`)
|
|
294
302
|
lines.push("")
|
|
295
303
|
}
|
|
296
304
|
}
|
|
@@ -309,17 +317,88 @@ export const read = tool({
|
|
|
309
317
|
},
|
|
310
318
|
})
|
|
311
319
|
|
|
320
|
+
export const read_by_id = tool({
|
|
321
|
+
description: "Read task by id.",
|
|
322
|
+
args: {
|
|
323
|
+
id: tool.schema.string().describe("Task ID"),
|
|
324
|
+
},
|
|
325
|
+
async execute(args, context) {
|
|
326
|
+
const todos = await readTodos(context.sessionID, context.directory)
|
|
327
|
+
const todo = todos.find(t => t.id === args.id)
|
|
328
|
+
if (!todo) return `❌ Task ${args.id} not found`
|
|
329
|
+
|
|
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")
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
|
|
312
390
|
export const update = tool({
|
|
313
|
-
description: "Update
|
|
391
|
+
description: "Update task(s). Send 1 or many for update",
|
|
314
392
|
args: {
|
|
315
393
|
todos: tool.schema.array(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
+
})
|
|
323
402
|
).describe("Array of todos to update"),
|
|
324
403
|
},
|
|
325
404
|
async execute(args, context) {
|