@comfanion/workflow 4.38.1 โ†’ 4.38.2

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.1",
3
+ "version": "4.38.2",
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.1",
3
- "buildDate": "2026-01-27T22:14:35.343Z",
2
+ "version": "4.38.2",
3
+ "buildDate": "2026-01-28T10:05:17.810Z",
4
4
  "files": [
5
5
  ".gitignore",
6
6
  "config.yaml",
@@ -382,198 +382,6 @@ pipeline:
382
382
  output:
383
383
  - docs/sprint-artifacts/sprint-N/retrospective.md
384
384
 
385
- # =============================================================================
386
- # AGENTS (Personas - WHO does work)
387
- # =============================================================================
388
- #
389
- # Model Strategy (based on Jan 2026 research):
390
- # - Architecture/Planning: Claude 4.5 Opus, Gemini 3 Pro (wisdom, context)
391
- # - Development: DeepSeek-V3.2 (cheapest, fast), GLM-4.7 (preserved thinking)
392
- # - Testing: GPT-5.2 Codex (best at finding bugs)
393
- # - UI/Frontend: MiniMax-M2.1 (fast, aesthetic)
394
- # - Research: Gemini 3 Pro (10M context, grounding)
395
- #
396
- agents:
397
- analyst:
398
- name: Sara
399
- title: Business Analyst
400
- icon: "๐Ÿ“Š"
401
- description: Requirements Analyst - extracts FR/NFR through stakeholder interviews
402
- mode: subagent
403
- model: anthropic/claude-sonnet-4-20250514
404
- temperature: 0.3
405
- file: agents/analyst.md
406
- expertise:
407
- - Requirements engineering
408
- - Business analysis
409
- - Stakeholder interviews
410
- personality: Methodical, thorough, asks clarifying questions
411
- skills_used:
412
- - requirements-gathering
413
- - requirements-validation
414
- - acceptance-criteria
415
- - unit-writing
416
- - methodologies
417
- - doc-todo
418
- - archiving
419
-
420
- pm:
421
- name: Dima
422
- title: Product Manager
423
- icon: "๐Ÿ“‹"
424
- description: Product Manager - creates PRDs, epics, stories, sprint planning, Jira sync
425
- mode: subagent
426
- model: anthropic/claude-sonnet-4-20250514
427
- temperature: 0.3
428
- file: agents/pm.md
429
- expertise:
430
- - Product management
431
- - B2B SaaS
432
- - Agile/Sprint planning
433
- personality: Business-focused, user-centric, data-driven
434
- skills_used:
435
- - prd-writing
436
- - prd-validation
437
- - acceptance-criteria
438
- - epic-writing
439
- - story-writing
440
- - sprint-planning
441
- - jira-integration
442
- - unit-writing
443
- - translation
444
- - methodologies
445
- - doc-todo
446
- - archiving
447
-
448
- architect:
449
- name: Winston
450
- title: Solution Architect
451
- icon: "๐Ÿ—๏ธ"
452
- description: Solution Architect - designs system architecture, makes technical decisions
453
- mode: subagent
454
- model: anthropic/claude-sonnet-4-20250514
455
- temperature: 0.2
456
- file: agents/architect.md
457
- expertise:
458
- - Distributed systems
459
- - Architecture patterns (chooses based on context)
460
- - System design
461
- personality: Technical, precise, patterns-focused
462
- skills_used:
463
- - architecture-design
464
- - architecture-validation
465
- - adr-writing
466
- - coding-standards
467
- - unit-writing
468
- - methodologies
469
- - api-design
470
- - database-design
471
- - diagram-creation
472
- - module-documentation
473
- - doc-todo
474
- - archiving
475
-
476
- dev:
477
- name: Rick
478
- title: Senior Developer
479
- icon: "๐Ÿ’ป"
480
- description: Developer - implements stories following red-green-refactor cycle
481
- mode: subagent
482
- model: anthropic/claude-sonnet-4-20250514
483
- temperature: 0.2
484
- file: agents/dev.md
485
- expertise:
486
- - Software development
487
- - TDD/BDD
488
- - Code review
489
- personality: Precise, test-driven, follows story specifications exactly
490
- skills_used:
491
- - dev-story
492
- - code-review
493
- - test-design
494
- - changelog
495
- - doc-todo
496
-
497
- coder:
498
- name: Morty
499
- title: Fast Coder
500
- icon: "โšก"
501
- description: Fast Coder - quick implementation, bug fixes, code following patterns
502
- mode: subagent
503
- hidden: true # Internal subagent, invoked by @dev
504
- model: anthropic/claude-sonnet-4-20250514
505
- temperature: 0.1
506
- file: agents/coder.md
507
- expertise:
508
- - Quick implementation
509
- - Bug fixes
510
- - Following existing patterns
511
- personality: Fast, no questions, executes or fails
512
- skills_used:
513
- - test-design
514
- - changelog
515
- - doc-todo
516
-
517
- reviewer:
518
- name: Marcus
519
- title: Code Reviewer
520
- icon: "๐Ÿ”"
521
- description: Code Reviewer - security-focused review, bug finding, test coverage
522
- mode: subagent
523
- model: openai/gpt-5.2-codex # Best at finding bugs and security issues
524
- temperature: 0.1
525
- file: agents/reviewer.md
526
- expertise:
527
- - Security review
528
- - Bug finding
529
- - Test coverage analysis
530
- - Code quality
531
- personality: Thorough, security-paranoid, always suggests fixes
532
- skills_used:
533
- - code-review
534
- auto_invoke:
535
- trigger: story_tasks_complete # Called automatically when all story tasks done
536
- before: story_marked_done
537
-
538
- # Supporting Agents (not in main pipeline)
539
- researcher:
540
- name: Kristina
541
- title: Researcher
542
- icon: "๐Ÿ”"
543
- description: Researcher - conducts technical, market, and domain research
544
- mode: subagent
545
- model: google/gemini-2.5-pro # Best for research - 1M context, grounding
546
- # Alternatives: gemini-2.5-flash (faster), gemini-3-pro (preview)
547
- temperature: 0.4
548
- file: agents/researcher.md
549
- expertise:
550
- - Technical research
551
- - Market analysis
552
- - Domain expertise
553
- - Deep research with web grounding
554
- personality: Curious, thorough, evidence-based
555
- skills_used:
556
- - research-methodology
557
- - methodologies
558
-
559
- change-manager:
560
- name: Bruce
561
- title: Change Manager
562
- icon: "๐Ÿ”„"
563
- description: Change Manager - manages documentation change proposals
564
- mode: subagent
565
- model: anthropic/claude-sonnet-4-20250514
566
- temperature: 0.2
567
- file: agents/change-manager.md
568
- expertise:
569
- - Change management
570
- - Impact analysis
571
- - Version control
572
- personality: Careful, systematic, risk-aware
573
- skills_used:
574
- - doc-todo
575
- - archiving
576
-
577
385
  # =============================================================================
578
386
  # ARTIFACTS
579
387
  # =============================================================================
@@ -701,95 +509,3 @@ quickstart:
701
509
  - step: 18
702
510
  command: /retrospective
703
511
  description: Sprint retrospective
704
-
705
- # =============================================================================
706
- # HOOKS & EVENTS (Plugin Integration)
707
- # =============================================================================
708
- #
709
- # Workflow emits events that plugins can subscribe to.
710
- # Plugins are in .opencode/plugins/ directory.
711
- #
712
- hooks:
713
- # Compaction hook - preserve context during session compaction
714
- compaction:
715
- plugin: custom-compaction.ts
716
- description: Preserves task/story status, critical files, continuation instructions
717
- context_files:
718
- always:
719
- - CLAUDE.md
720
- - AGENTS.md
721
- - project-context.md
722
- - .opencode/config.yaml
723
- if_exists:
724
- - docs/prd.md
725
- - docs/architecture.md
726
- - docs/coding-standards/README.md
727
- - docs/coding-standards/patterns.md
728
- dynamic:
729
- - active_story_file # From sprint-status.yaml
730
- - modified_files # Files edited in session
731
-
732
- events:
733
- # Session lifecycle
734
- session:
735
- - session.started # New session began
736
- - session.idle # Agent finished responding
737
- - session.compacted # Session was compacted
738
- - session.error # Error occurred
739
-
740
- # Agent lifecycle
741
- agent:
742
- - agent.activated # Agent persona loaded
743
- - agent.skill.loaded # Skill file was read
744
- - agent.task.started # Agent started a task
745
- - agent.task.completed # Agent completed a task
746
-
747
- # Workflow pipeline
748
- workflow:
749
- - workflow.stage.started # Pipeline stage began
750
- - workflow.stage.completed # Pipeline stage finished
751
- - workflow.stage.skipped # Optional stage skipped
752
- - workflow.validation.passed # Validation succeeded
753
- - workflow.validation.failed # Validation failed
754
-
755
- # Document lifecycle
756
- document:
757
- - document.created # New doc created
758
- - document.updated # Doc modified
759
- - document.validated # Doc passed validation
760
- - document.archived # Doc moved to archive
761
-
762
- # Sprint/Story lifecycle
763
- sprint:
764
- - epic.created # Epic document created
765
- - story.created # Story document created
766
- - story.started # Story status โ†’ in-progress
767
- - story.task.completed # Task marked [x]
768
- - story.completed # All tasks done, status โ†’ review
769
- - story.approved # Code review passed, status โ†’ done
770
- - sprint.planned # Sprint planning completed
771
- - sprint.completed # All stories done
772
-
773
- # Jira integration
774
- jira:
775
- - jira.sync.started # Sync began
776
- - jira.sync.completed # Sync finished
777
- - jira.issue.created # New issue in Jira
778
- - jira.issue.updated # Issue updated
779
- - jira.transition # Status changed
780
-
781
- # Example plugin subscriptions
782
- #
783
- # .opencode/plugins/notifications.ts:
784
- # event: async ({ event }) => {
785
- # if (event.type === "story.completed") {
786
- # await sendSlackNotification(event.data)
787
- # }
788
- # }
789
- #
790
- # .opencode/plugins/jira-auto-sync.ts:
791
- # event: async ({ event }) => {
792
- # if (event.type === "story.task.completed") {
793
- # await updateJiraProgress(event.data)
794
- # }
795
- # }
@@ -27,9 +27,6 @@ documentation:
27
27
  enforced: true # Cannot be changed
28
28
  paths:
29
29
  - "docs/"
30
- - "CLAUDE.md"
31
- - "README.md"
32
- - ".opencode/"
33
30
 
34
31
  # User-facing docs (translations, Confluence export)
35
32
  user_facing:
@@ -83,13 +80,6 @@ workflow:
83
80
  # Default validation level: strict | normal | minimal
84
81
  validation_level: normal
85
82
 
86
- # =============================================================================
87
- # AGENT DEFAULTS
88
- # =============================================================================
89
- agents:
90
- default_model: "anthropic/claude-sonnet-4-20250514"
91
- default_temperature: 0.3
92
-
93
83
  # =============================================================================
94
84
  # JIRA INTEGRATION
95
85
  # =============================================================================
@@ -21,7 +21,8 @@
21
21
  "tools": {
22
22
  "lsp": true,
23
23
  "search": true,
24
- "codeindex": true
24
+ "codeindex": true,
25
+ "usethis-todo": true
25
26
  },
26
27
 
27
28
  "permission": {
@@ -4,6 +4,6 @@
4
4
  "test:leak": "bun test --smol plugins/__tests__/ --test-name-pattern 'memory safety'"
5
5
  },
6
6
  "dependencies": {
7
- "@opencode-ai/plugin": "1.1.36"
7
+ "@opencode-ai/plugin": "1.1.39"
8
8
  }
9
9
  }
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test"
2
+ import { readFile } from "fs/promises"
3
+ import { join } from "path"
4
+ import { createTempDir, cleanupTempDir } from "./helpers/mock-ctx"
5
+ import { write, update, read } from "../../tools/usethis_todo"
6
+
7
+ describe("usethis_todo tool", () => {
8
+ let tempDir: string
9
+ let originalXdg: string | undefined
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await createTempDir()
13
+ originalXdg = process.env.XDG_DATA_HOME
14
+ process.env.XDG_DATA_HOME = join(tempDir, "xdg-data")
15
+ })
16
+
17
+ afterEach(async () => {
18
+ if (originalXdg === undefined) {
19
+ delete process.env.XDG_DATA_HOME
20
+ } else {
21
+ process.env.XDG_DATA_HOME = originalXdg
22
+ }
23
+ await cleanupTempDir(tempDir)
24
+ })
25
+
26
+ it("writes enhanced and native todo files", async () => {
27
+ const ctx = { sessionID: "sess-test", directory: tempDir } as any
28
+ const output = await write.execute(
29
+ {
30
+ todos: [
31
+ { id: "A1", content: "First task", status: "ready", priority: "HIGH" },
32
+ { id: "A2", content: "Second task", status: "pending", priority: "LOW" },
33
+ ],
34
+ },
35
+ ctx
36
+ )
37
+
38
+ expect(output).toContain("TODO Graph")
39
+
40
+ const enhancedPath = join(tempDir, ".opencode", "session-todos", "sess-test.json")
41
+ const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
42
+ expect(enhanced.length).toBe(2)
43
+ expect(enhanced[0].content).toBe("First task")
44
+
45
+ const nativePath = join(
46
+ process.env.XDG_DATA_HOME!,
47
+ "opencode",
48
+ "storage",
49
+ "todo",
50
+ "sess-test.json"
51
+ )
52
+ const native = JSON.parse(await readFile(nativePath, "utf-8"))
53
+ expect(native.length).toBe(2)
54
+ expect(native[0].content).toContain("First task")
55
+ })
56
+
57
+ it("update merges by id and can add new tasks", async () => {
58
+ const ctx = { sessionID: "sess-merge", directory: tempDir } as any
59
+
60
+ await write.execute(
61
+ {
62
+ todos: [
63
+ { id: "A1", content: "First task", status: "pending", priority: "MED" },
64
+ ],
65
+ },
66
+ ctx
67
+ )
68
+
69
+ const result = await update.execute(
70
+ {
71
+ todos: [
72
+ { id: "A1", content: "First task", status: "done", priority: "MED" },
73
+ { id: "A2", content: "Second task", status: "ready", priority: "LOW" },
74
+ ],
75
+ },
76
+ ctx
77
+ )
78
+
79
+ expect(result).toContain("Updated 2 task(s)")
80
+
81
+ const enhancedPath = join(tempDir, ".opencode", "session-todos", "sess-merge.json")
82
+ const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
83
+ expect(enhanced.length).toBe(2)
84
+ expect(enhanced.find((t: any) => t.id === "A1")?.status).toBe("done")
85
+ })
86
+
87
+ it("read returns graph with content", async () => {
88
+ const ctx = { sessionID: "sess-read", directory: tempDir } as any
89
+ await write.execute(
90
+ {
91
+ todos: [
92
+ { id: "A1", content: "Task content", status: "ready", priority: "HIGH" },
93
+ ],
94
+ },
95
+ ctx
96
+ )
97
+
98
+ const output = await read.execute({}, ctx)
99
+ expect(output).toContain("Task content")
100
+ expect(output).toContain("Available Now")
101
+ })
102
+ })
@@ -0,0 +1,36 @@
1
+ import type { Plugin } from "@opencode-ai/plugin"
2
+
3
+ /**
4
+ * Publishes a TODO snapshot into the session chat.
5
+ *
6
+ * Why: the TODO sidebar may not live-refresh when we write native todo files
7
+ * (no todo.updated Bus event). This makes changes visible in the main dialog.
8
+ */
9
+ export const UsethisTodoPublish: Plugin = async ({ client }) => {
10
+ return {
11
+ "tool.execute.after": async (input, output) => {
12
+ if (input.tool !== "usethis_todo_write" && input.tool !== "usethis_todo_update") return
13
+
14
+ const text = [
15
+ `## TODO`,
16
+ // `session: ${input.sessionID}`,
17
+ "",
18
+ output.output
19
+ ].join("\n")
20
+
21
+ try {
22
+ await client.session.prompt({
23
+ path: { id: input.sessionID },
24
+ body: {
25
+ noReply: true,
26
+ parts: [{ type: "text", text }],
27
+ },
28
+ })
29
+ } catch {}
30
+
31
+ // Debug toasts removed
32
+ },
33
+ }
34
+ }
35
+
36
+ export default UsethisTodoPublish
@@ -0,0 +1,37 @@
1
+ import type { Plugin } from "@opencode-ai/plugin"
2
+
3
+ /**
4
+ * UseThis TODO UI Plugin โ€” zero-state
5
+ *
6
+ * ONLY sets output.title for usethis_todo_* tools in TUI.
7
+ * No caching, no env vars, no before hook, no HTTP calls.
8
+ * Parses tool output string directly in after hook.
9
+ */
10
+
11
+ export const UsethisTodoUIPlugin: Plugin = async () => {
12
+ return {
13
+ "tool.execute.after": async (input, output) => {
14
+ if (!input.tool.startsWith("usethis_todo_")) return
15
+
16
+ const out = output.output || ""
17
+
18
+ if (input.tool === "usethis_todo_write") {
19
+ const match = out.match(/\[(\d+)\/(\d+) done/)
20
+ output.title = match ? `๐Ÿ“‹ TODO: ${match[2]} tasks` : "๐Ÿ“‹ TODO updated"
21
+
22
+ } else if (input.tool === "usethis_todo_update") {
23
+ const match = out.match(/^โœ… (.+)$/m)
24
+ output.title = match ? `๐Ÿ“ ${match[1]}` : "๐Ÿ“ Task updated"
25
+
26
+ } else if (input.tool === "usethis_todo_read") {
27
+ const match = out.match(/\[(\d+)\/(\d+) done, (\d+) in progress\]/)
28
+ output.title = match ? `๐Ÿ“‹ TODO [${match[1]}/${match[2]} done]` : "๐Ÿ“‹ TODO list"
29
+
30
+ } else if (input.tool === "usethis_todo_read_next_five") {
31
+ output.title = "๐Ÿ“‹ Next 5 tasks"
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ export default UsethisTodoUIPlugin
@@ -0,0 +1,344 @@
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_next_five() - get next 5 available tasks
8
+ * usethis_todo_update(id, field, value) - update any task field
9
+ *
10
+ * Storage:
11
+ * Enhanced: .opencode/session-todos/{sid}.json (title, blockedBy, graph)
12
+ * Native: ~/.local/share/opencode/storage/todo/{sid}.json (TUI display)
13
+ *
14
+ * Features:
15
+ * - Hierarchical IDs: E01-S01-T01
16
+ * - Dependencies: blockedBy field
17
+ * - Priority: CRIT | HIGH | MED | LOW (auto-sorted)
18
+ * - Graph: shows available, blocked, parallel tasks
19
+ * - Dual write: native OpenCode storage for TUI integration
20
+ */
21
+
22
+ import { tool } from "@opencode-ai/plugin"
23
+ import path from "path"
24
+ import os from "os"
25
+ import fs from "fs/promises"
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ interface Todo {
32
+ id: string // E01-S01-T01
33
+ content: string // Full task description
34
+ status: string // pending | ready | in_progress | waiting_review | done | cancelled
35
+ priority: string // CRIT | HIGH | MED | LOW
36
+ blockedBy?: string[] // IDs of blocking tasks
37
+ createdAt?: number
38
+ updatedAt?: number
39
+ }
40
+
41
+ interface NativeTodo {
42
+ id: string
43
+ content: string // "title: content" combined
44
+ status: string // pending | in_progress | completed | cancelled
45
+ priority: string // high | medium | low
46
+ }
47
+
48
+ interface TodoGraph {
49
+ todos: Todo[]
50
+ available: string[]
51
+ parallel: string[][]
52
+ blocked: Record<string, string[]>
53
+ }
54
+
55
+ // ============================================================================
56
+ // Storage โ€” dual write
57
+ // ============================================================================
58
+
59
+ // Resolve project directory (context.directory may be undefined via MCP)
60
+ function dir(directory?: string): string {
61
+ return directory || process.env.OPENCODE_PROJECT_DIR || process.cwd()
62
+ }
63
+
64
+ // Enhanced storage path (project-local)
65
+ function getEnhancedPath(sid: string, directory?: string): string {
66
+ return path.join(dir(directory), ".opencode", "session-todos", `${sid || "current"}.json`)
67
+ }
68
+
69
+ async function getNativeDataDirs(): Promise<string[]> {
70
+ const dirs = new Set<string>()
71
+
72
+ // 1) xdg-basedir (what OpenCode itself uses)
73
+ try {
74
+ const mod: any = await import("xdg-basedir")
75
+ if (mod?.xdgData && typeof mod.xdgData === "string") {
76
+ dirs.add(mod.xdgData)
77
+ }
78
+ } catch {
79
+ // ignore
80
+ }
81
+
82
+ // 2) explicit XDG override
83
+ if (process.env.XDG_DATA_HOME) {
84
+ dirs.add(process.env.XDG_DATA_HOME)
85
+ }
86
+
87
+ // 3) common fallbacks
88
+ dirs.add(path.join(os.homedir(), ".local", "share"))
89
+ dirs.add(path.join(os.homedir(), "Library", "Application Support"))
90
+
91
+ return [...dirs]
92
+ }
93
+
94
+ async function getNativePaths(sid: string): Promise<string[]> {
95
+ const baseDirs = await getNativeDataDirs()
96
+ const file = `${sid || "current"}.json`
97
+ return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
98
+ }
99
+
100
+ // Map our format โ†’ native format
101
+ function toNative(todo: Todo): NativeTodo {
102
+ // Status mapping: our โ†’ native
103
+ const statusMap: Record<string, string> = {
104
+ pending: "pending",
105
+ ready: "pending", // native has no "ready"
106
+ in_progress: "in_progress",
107
+ waiting_review: "in_progress", // native has no "waiting_review"
108
+ done: "completed", // native uses "completed" not "done"
109
+ cancelled: "cancelled",
110
+ }
111
+ // Priority mapping: CRIT/HIGH/MED/LOW โ†’ high/medium/low
112
+ const prioMap: Record<string, string> = {
113
+ CRIT: "high",
114
+ HIGH: "high",
115
+ MED: "medium",
116
+ LOW: "low",
117
+ }
118
+
119
+ const deps = todo.blockedBy?.length ? ` [โ† ${todo.blockedBy.join(", ")}]` : ""
120
+
121
+ return {
122
+ id: todo.id,
123
+ content: `${todo.content}${deps}`,
124
+ status: statusMap[todo.status] || "pending",
125
+ priority: prioMap[todo.priority] || "medium",
126
+ }
127
+ }
128
+
129
+ async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
130
+ try {
131
+ return JSON.parse(await fs.readFile(getEnhancedPath(sid, directory), "utf-8"))
132
+ } catch {
133
+ return []
134
+ }
135
+ }
136
+
137
+ async function writeTodos(todos: Todo[], sid: string, directory?: string): Promise<void> {
138
+ // 1. Enhanced storage (our full format)
139
+ const enhancedPath = getEnhancedPath(sid, directory)
140
+ await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
141
+ await fs.writeFile(enhancedPath, JSON.stringify(todos, null, 2), "utf-8")
142
+
143
+ // 2. Native storage (for TUI display)
144
+ const nativeTodos = todos.map(toNative)
145
+ try {
146
+ const nativePaths = await getNativePaths(sid)
147
+ await Promise.allSettled(
148
+ nativePaths.map(async (nativePath) => {
149
+ await fs.mkdir(path.dirname(nativePath), { recursive: true })
150
+ await fs.writeFile(nativePath, JSON.stringify(nativeTodos, null, 2), "utf-8")
151
+ }),
152
+ )
153
+ } catch {
154
+ // Native write failure is non-fatal
155
+ }
156
+ }
157
+
158
+ // ============================================================================
159
+ // Graph analysis
160
+ // ============================================================================
161
+
162
+ function analyzeGraph(todos: Todo[]): TodoGraph {
163
+ const blocked: Record<string, string[]> = {}
164
+ const availableTodos: Todo[] = []
165
+
166
+ for (const todo of todos) {
167
+ if (todo.status !== "ready") continue
168
+ const activeBlockers = (todo.blockedBy || []).filter(id => {
169
+ const b = todos.find(t => t.id === id)
170
+ return b && b.status !== "done"
171
+ })
172
+ if (activeBlockers.length === 0) {
173
+ availableTodos.push(todo)
174
+ } else {
175
+ blocked[todo.id] = activeBlockers
176
+ }
177
+ }
178
+
179
+ const P: Record<string, number> = { CRIT: 0, HIGH: 1, MED: 2, LOW: 3 }
180
+ availableTodos.sort((a, b) => (P[a.priority] ?? 2) - (P[b.priority] ?? 2))
181
+ const available = availableTodos.map(t => t.id)
182
+
183
+ // Parallel groups
184
+ const parallel: string[][] = []
185
+ const seen = new Set<string>()
186
+ for (const id of available) {
187
+ if (seen.has(id)) continue
188
+ const group = [id]
189
+ seen.add(id)
190
+ for (const other of available) {
191
+ if (seen.has(other)) continue
192
+ const a = todos.find(t => t.id === id)
193
+ const b = todos.find(t => t.id === other)
194
+ if (!b?.blockedBy?.includes(id) && !a?.blockedBy?.includes(other)) {
195
+ group.push(other)
196
+ seen.add(other)
197
+ }
198
+ }
199
+ if (group.length > 0) parallel.push(group)
200
+ }
201
+
202
+ return { todos, available, parallel, blocked }
203
+ }
204
+
205
+ // ============================================================================
206
+ // Formatting
207
+ // ============================================================================
208
+
209
+ const PE = (p?: string) => p === "CRIT" ? "๐Ÿ”ด" : p === "HIGH" ? "๐ŸŸ " : p === "LOW" ? "๐ŸŸข" : "๐ŸŸก"
210
+ const SI = (s: string) => s === "done" ? "โœ“" : s === "in_progress" ? "โš™" : s === "ready" ? "โ—‹" : s === "cancelled" ? "โœ—" : s === "waiting_review" ? "โณ" : "ยท"
211
+
212
+ function formatGraph(graph: TodoGraph): string {
213
+ const { todos } = graph
214
+ const total = todos.length
215
+ const done = todos.filter(t => t.status === "done").length
216
+ const wip = todos.filter(t => t.status === "in_progress").length
217
+
218
+ const lines: string[] = [`โ•โ•โ• TODO Graph [${done}/${total} done, ${wip} in progress] โ•โ•โ•`, ""]
219
+
220
+ lines.push("All Tasks:")
221
+ for (const t of todos) {
222
+ const deps = t.blockedBy?.length ? ` โ† ${t.blockedBy.join(", ")}` : ""
223
+ lines.push(` ${SI(t.status)} ${PE(t.priority)} ${t.id}: ${t.content}${deps}`)
224
+ }
225
+ lines.push("")
226
+
227
+ if (graph.available.length > 0) {
228
+ lines.push("Available Now:")
229
+ for (const id of graph.available) {
230
+ const t = todos.find(x => x.id === id)
231
+ lines.push(` โ†’ ${PE(t?.priority)} ${id}: ${t?.content}`)
232
+ }
233
+ } else {
234
+ lines.push("Available Now: none")
235
+ }
236
+ lines.push("")
237
+
238
+ const multi = graph.parallel.filter(g => g.length > 1)
239
+ if (multi.length > 0) {
240
+ lines.push("Parallel Groups:")
241
+ multi.forEach((g, i) => lines.push(` Group ${i + 1}: ${g.join(", ")}`))
242
+ lines.push("")
243
+ }
244
+
245
+ if (Object.keys(graph.blocked).length > 0) {
246
+ lines.push("Blocked:")
247
+ for (const [id, blockers] of Object.entries(graph.blocked)) {
248
+ const t = todos.find(x => x.id === id)
249
+ lines.push(` โŠ— ${id}: ${t?.content} โ† waiting: ${blockers.join(", ")}`)
250
+ }
251
+ }
252
+
253
+ return lines.join("\n")
254
+ }
255
+
256
+ // ============================================================================
257
+ // Tools
258
+ // ============================================================================
259
+
260
+ export const write = tool({
261
+ description: "Create or update TODO list. TODOv2",
262
+ args: {
263
+ todos: tool.schema.array(
264
+ tool.schema.object({
265
+ id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
266
+ content: tool.schema.string().describe("Full task description"),
267
+ status: tool.schema.string().describe("pending | ready | in_progress | waiting_review | done | cancelled"),
268
+ priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
269
+ blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
270
+ })
271
+ ).describe("Array of todos"),
272
+ },
273
+ async execute(args, context) {
274
+ const now = Date.now()
275
+ const todos = args.todos.map(t => ({ ...t, createdAt: t.createdAt || now, updatedAt: now }))
276
+ await writeTodos(todos, context.sessionID, context.directory)
277
+ return formatGraph(analyzeGraph(todos))
278
+ },
279
+ })
280
+
281
+ export const read_next_five = tool({
282
+ description: "Read current TODO list. Shows Next 5 tasks.",
283
+ args: {},
284
+ async execute(_args, context) {
285
+ const todos = await readTodos(context.sessionID, context.directory)
286
+ const graph = analyzeGraph(todos)
287
+ if (graph.available.length === 0) return "No tasks available. All blocked or not ready."
288
+ const next5 = graph.available.slice(0, 5)
289
+ const lines: string[] = ["Next 5 available tasks:", ""]
290
+ for (const id of next5) {
291
+ const t = graph.todos.find(x => x.id === id)
292
+ if (t) {
293
+ lines.push(`${PE(t.priority)} ${id}: ${t.content}`)
294
+ lines.push("")
295
+ }
296
+ }
297
+ if (graph.available.length > 5) lines.push(`... +${graph.available.length - 5} more`)
298
+ return lines.join("\n")
299
+ },
300
+ })
301
+
302
+ export const read = tool({
303
+ description: "Read current TODO list. Shows all tasks.",
304
+ args: {},
305
+ async execute(_args, context) {
306
+ const todos = await readTodos(context.sessionID, context.directory)
307
+ if (todos.length === 0) return "No todos. Use usethis_todo_write to create."
308
+ return formatGraph(analyzeGraph(todos))
309
+ },
310
+ })
311
+
312
+ export const update = tool({
313
+ description: "Update tasks. Same interface as write, but merges by id.",
314
+ args: {
315
+ todos: tool.schema.array(
316
+ tool.schema.object({
317
+ id: tool.schema.string().describe("Task ID in concat format: E01-S01-T01"),
318
+ content: tool.schema.string().describe("Full task description"),
319
+ status: tool.schema.string().describe("pending | ready | in_progress | waiting_review | done | cancelled"),
320
+ priority: tool.schema.string().describe("CRIT | HIGH | MED | LOW"),
321
+ blockedBy: tool.schema.array(tool.schema.string()).optional().describe("IDs of blocking tasks"),
322
+ })
323
+ ).describe("Array of todos to update"),
324
+ },
325
+ async execute(args, context) {
326
+ const todos = await readTodos(context.sessionID, context.directory)
327
+ const now = Date.now()
328
+ const byId = new Map(todos.map(t => [t.id, t]))
329
+
330
+ for (const incoming of args.todos) {
331
+ const existing = byId.get(incoming.id)
332
+ if (existing) {
333
+ Object.assign(existing, incoming)
334
+ existing.updatedAt = now
335
+ } else {
336
+ byId.set(incoming.id, { ...incoming, createdAt: now, updatedAt: now })
337
+ }
338
+ }
339
+
340
+ const merged = [...byId.values()]
341
+ await writeTodos(merged, context.sessionID, context.directory)
342
+ return `โœ… Updated ${args.todos.length} task(s)\n\n${formatGraph(analyzeGraph(merged))}`
343
+ },
344
+ })