@comfanion/workflow 4.38.1 → 4.38.3-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 +2 -2
- package/src/opencode/FLOW.yaml +0 -284
- 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 -11
- package/src/opencode/opencode.json +2 -1
- package/src/opencode/package.json +1 -1
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +138 -0
- package/src/opencode/plugins/usethis-todo-publish.ts +36 -0
- package/src/opencode/plugins/usethis-todo-ui.ts +37 -0
- package/src/opencode/tools/usethis_todo.ts +375 -0
package/package.json
CHANGED
package/src/build-info.json
CHANGED
package/src/opencode/FLOW.yaml
CHANGED
|
@@ -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
|
-
# }
|
|
@@ -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
|
@@ -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
|
# =============================================================================
|
|
@@ -221,7 +211,7 @@ epic_workflow:
|
|
|
221
211
|
# Run integration tests after each story
|
|
222
212
|
# When true: story done → run integration tests → continue
|
|
223
213
|
# When false: skip per-story integration tests
|
|
224
|
-
test_after_each_story:
|
|
214
|
+
test_after_each_story: true
|
|
225
215
|
|
|
226
216
|
# Run integration tests after epic complete
|
|
227
217
|
# When true: all stories done → run epic integration tests
|
|
@@ -0,0 +1,138 @@
|
|
|
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, get_by_id } 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", description: "Longer details", 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
|
+
expect(enhanced[0].description).toBe("Longer details")
|
|
45
|
+
|
|
46
|
+
const nativePath = join(
|
|
47
|
+
process.env.XDG_DATA_HOME!,
|
|
48
|
+
"opencode",
|
|
49
|
+
"storage",
|
|
50
|
+
"todo",
|
|
51
|
+
"sess-test.json"
|
|
52
|
+
)
|
|
53
|
+
const native = JSON.parse(await readFile(nativePath, "utf-8"))
|
|
54
|
+
expect(native.length).toBe(2)
|
|
55
|
+
expect(native[0].content).toContain("First task")
|
|
56
|
+
expect(native[0].content).toContain("Longer details")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("update merges by id and can add new tasks", async () => {
|
|
60
|
+
const ctx = { sessionID: "sess-merge", directory: tempDir } as any
|
|
61
|
+
|
|
62
|
+
await write.execute(
|
|
63
|
+
{
|
|
64
|
+
todos: [
|
|
65
|
+
{ id: "A1", content: "First task", description: "v1", status: "pending", priority: "MED" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
ctx
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const result = await update.execute(
|
|
72
|
+
{
|
|
73
|
+
todos: [
|
|
74
|
+
{ id: "A1", content: "First task", description: "v2", status: "done", priority: "MED" },
|
|
75
|
+
{ id: "A2", content: "Second task", status: "ready", priority: "LOW" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
ctx
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(result).toContain("Updated 2 task(s)")
|
|
82
|
+
|
|
83
|
+
const enhancedPath = join(tempDir, ".opencode", "session-todos", "sess-merge.json")
|
|
84
|
+
const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
|
|
85
|
+
expect(enhanced.length).toBe(2)
|
|
86
|
+
expect(enhanced.find((t: any) => t.id === "A1")?.status).toBe("done")
|
|
87
|
+
expect(enhanced.find((t: any) => t.id === "A1")?.description).toBe("v2")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("read returns graph with content", async () => {
|
|
91
|
+
const ctx = { sessionID: "sess-read", directory: tempDir } as any
|
|
92
|
+
await write.execute(
|
|
93
|
+
{
|
|
94
|
+
todos: [
|
|
95
|
+
{ id: "A1", content: "Task content", status: "ready", priority: "HIGH" },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
ctx
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const output = await read.execute({}, ctx)
|
|
102
|
+
expect(output).toContain("Task content")
|
|
103
|
+
expect(output).toContain("Available Now")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("get_by_id returns single task", async () => {
|
|
107
|
+
const ctx = { sessionID: "sess-get", directory: tempDir } as any
|
|
108
|
+
await write.execute(
|
|
109
|
+
{
|
|
110
|
+
todos: [
|
|
111
|
+
{
|
|
112
|
+
id: "A1",
|
|
113
|
+
content: "Task content",
|
|
114
|
+
description: "More details",
|
|
115
|
+
status: "ready",
|
|
116
|
+
priority: "HIGH",
|
|
117
|
+
blockedBy: ["B1"],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
ctx
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const out = await get_by_id.execute({ id: "A1" }, ctx)
|
|
125
|
+
expect(out).toContain("id: A1")
|
|
126
|
+
expect(out).toContain("content:")
|
|
127
|
+
expect(out).toContain("Task content")
|
|
128
|
+
expect(out).toContain("blockedBy: B1")
|
|
129
|
+
expect(out).toContain("description:")
|
|
130
|
+
expect(out).toContain("More details")
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("get_by_id returns not found", async () => {
|
|
134
|
+
const ctx = { sessionID: "sess-miss", directory: tempDir } as any
|
|
135
|
+
const out = await get_by_id.execute({ id: "NOPE" }, ctx)
|
|
136
|
+
expect(out).toContain("not found")
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -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,375 @@
|
|
|
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 // Short task summary
|
|
34
|
+
description?: string // Full task description (optional)
|
|
35
|
+
status: string // pending | ready | in_progress | waiting_review | done | cancelled
|
|
36
|
+
priority: string // CRIT | HIGH | MED | LOW
|
|
37
|
+
blockedBy?: string[] // IDs of blocking tasks
|
|
38
|
+
createdAt?: number
|
|
39
|
+
updatedAt?: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface NativeTodo {
|
|
43
|
+
id: string
|
|
44
|
+
content: string // "title: content" combined
|
|
45
|
+
status: string // pending | in_progress | completed | cancelled
|
|
46
|
+
priority: string // high | medium | low
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface TodoGraph {
|
|
50
|
+
todos: Todo[]
|
|
51
|
+
available: string[]
|
|
52
|
+
parallel: string[][]
|
|
53
|
+
blocked: Record<string, string[]>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Storage — dual write
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
// Resolve project directory (context.directory may be undefined via MCP)
|
|
61
|
+
function dir(directory?: string): string {
|
|
62
|
+
return directory || process.env.OPENCODE_PROJECT_DIR || process.cwd()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Enhanced storage path (project-local)
|
|
66
|
+
function getEnhancedPath(sid: string, directory?: string): string {
|
|
67
|
+
return path.join(dir(directory), ".opencode", "session-todos", `${sid || "current"}.json`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function getNativeDataDirs(): Promise<string[]> {
|
|
71
|
+
const dirs = new Set<string>()
|
|
72
|
+
|
|
73
|
+
// 1) xdg-basedir (what OpenCode itself uses)
|
|
74
|
+
try {
|
|
75
|
+
const mod: any = await import("xdg-basedir")
|
|
76
|
+
if (mod?.xdgData && typeof mod.xdgData === "string") {
|
|
77
|
+
dirs.add(mod.xdgData)
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2) explicit XDG override
|
|
84
|
+
if (process.env.XDG_DATA_HOME) {
|
|
85
|
+
dirs.add(process.env.XDG_DATA_HOME)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3) common fallbacks
|
|
89
|
+
dirs.add(path.join(os.homedir(), ".local", "share"))
|
|
90
|
+
dirs.add(path.join(os.homedir(), "Library", "Application Support"))
|
|
91
|
+
|
|
92
|
+
return [...dirs]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function getNativePaths(sid: string): Promise<string[]> {
|
|
96
|
+
const baseDirs = await getNativeDataDirs()
|
|
97
|
+
const file = `${sid || "current"}.json`
|
|
98
|
+
return baseDirs.map((base) => path.join(base, "opencode", "storage", "todo", file))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Map our format → native format
|
|
102
|
+
function toNative(todo: Todo): NativeTodo {
|
|
103
|
+
// Status mapping: our → native
|
|
104
|
+
const statusMap: Record<string, string> = {
|
|
105
|
+
pending: "pending",
|
|
106
|
+
ready: "pending", // native has no "ready"
|
|
107
|
+
in_progress: "in_progress",
|
|
108
|
+
waiting_review: "in_progress", // native has no "waiting_review"
|
|
109
|
+
done: "completed", // native uses "completed" not "done"
|
|
110
|
+
cancelled: "cancelled",
|
|
111
|
+
}
|
|
112
|
+
// Priority mapping: CRIT/HIGH/MED/LOW → high/medium/low
|
|
113
|
+
const prioMap: Record<string, string> = {
|
|
114
|
+
CRIT: "high",
|
|
115
|
+
HIGH: "high",
|
|
116
|
+
MED: "medium",
|
|
117
|
+
LOW: "low",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const deps = todo.blockedBy?.length ? ` [← ${todo.blockedBy.join(", ")}]` : ""
|
|
121
|
+
const desc = todo.description?.trim() ? ` — ${todo.description.trim()}` : ""
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
id: todo.id,
|
|
125
|
+
content: `${todo.content}${desc}${deps}`,
|
|
126
|
+
status: statusMap[todo.status] || "pending",
|
|
127
|
+
priority: prioMap[todo.priority] || "medium",
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readTodos(sid: string, directory?: string): Promise<Todo[]> {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(await fs.readFile(getEnhancedPath(sid, directory), "utf-8"))
|
|
134
|
+
} catch {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeTodos(todos: Todo[], sid: string, directory?: string): Promise<void> {
|
|
140
|
+
// 1. Enhanced storage (our full format)
|
|
141
|
+
const enhancedPath = getEnhancedPath(sid, directory)
|
|
142
|
+
await fs.mkdir(path.dirname(enhancedPath), { recursive: true })
|
|
143
|
+
await fs.writeFile(enhancedPath, JSON.stringify(todos, null, 2), "utf-8")
|
|
144
|
+
|
|
145
|
+
// 2. Native storage (for TUI display)
|
|
146
|
+
const nativeTodos = todos.map(toNative)
|
|
147
|
+
try {
|
|
148
|
+
const nativePaths = await getNativePaths(sid)
|
|
149
|
+
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
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
} catch {
|
|
156
|
+
// Native write failure is non-fatal
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Graph analysis
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
function analyzeGraph(todos: Todo[]): TodoGraph {
|
|
165
|
+
const blocked: Record<string, string[]> = {}
|
|
166
|
+
const availableTodos: Todo[] = []
|
|
167
|
+
|
|
168
|
+
for (const todo of todos) {
|
|
169
|
+
if (todo.status !== "ready") continue
|
|
170
|
+
const activeBlockers = (todo.blockedBy || []).filter(id => {
|
|
171
|
+
const b = todos.find(t => t.id === id)
|
|
172
|
+
return b && b.status !== "done"
|
|
173
|
+
})
|
|
174
|
+
if (activeBlockers.length === 0) {
|
|
175
|
+
availableTodos.push(todo)
|
|
176
|
+
} else {
|
|
177
|
+
blocked[todo.id] = activeBlockers
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const P: Record<string, number> = { CRIT: 0, HIGH: 1, MED: 2, LOW: 3 }
|
|
182
|
+
availableTodos.sort((a, b) => (P[a.priority] ?? 2) - (P[b.priority] ?? 2))
|
|
183
|
+
const available = availableTodos.map(t => t.id)
|
|
184
|
+
|
|
185
|
+
// Parallel groups
|
|
186
|
+
const parallel: string[][] = []
|
|
187
|
+
const seen = new Set<string>()
|
|
188
|
+
for (const id of available) {
|
|
189
|
+
if (seen.has(id)) continue
|
|
190
|
+
const group = [id]
|
|
191
|
+
seen.add(id)
|
|
192
|
+
for (const other of available) {
|
|
193
|
+
if (seen.has(other)) continue
|
|
194
|
+
const a = todos.find(t => t.id === id)
|
|
195
|
+
const b = todos.find(t => t.id === other)
|
|
196
|
+
if (!b?.blockedBy?.includes(id) && !a?.blockedBy?.includes(other)) {
|
|
197
|
+
group.push(other)
|
|
198
|
+
seen.add(other)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (group.length > 0) parallel.push(group)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { todos, available, parallel, blocked }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Formatting
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
const PE = (p?: string) => p === "CRIT" ? "🔴" : p === "HIGH" ? "🟠" : p === "LOW" ? "🟢" : "🟡"
|
|
212
|
+
const SI = (s: string) => s === "done" ? "✓" : s === "in_progress" ? "⚙" : s === "ready" ? "○" : s === "cancelled" ? "✗" : s === "waiting_review" ? "⏳" : "·"
|
|
213
|
+
|
|
214
|
+
function formatGraph(graph: TodoGraph): string {
|
|
215
|
+
const { todos } = graph
|
|
216
|
+
const total = todos.length
|
|
217
|
+
const done = todos.filter(t => t.status === "done").length
|
|
218
|
+
const wip = todos.filter(t => t.status === "in_progress").length
|
|
219
|
+
|
|
220
|
+
const lines: string[] = [`═══ TODO Graph [${done}/${total} done, ${wip} in progress] ═══`, ""]
|
|
221
|
+
|
|
222
|
+
lines.push("All Tasks:")
|
|
223
|
+
for (const t of todos) {
|
|
224
|
+
const deps = t.blockedBy?.length ? ` ← ${t.blockedBy.join(", ")}` : ""
|
|
225
|
+
const desc = t.description?.trim() ? ` — ${t.description.trim()}` : ""
|
|
226
|
+
lines.push(` ${SI(t.status)} ${PE(t.priority)} ${t.id}: ${t.content}${desc}${deps}`)
|
|
227
|
+
}
|
|
228
|
+
lines.push("")
|
|
229
|
+
|
|
230
|
+
if (graph.available.length > 0) {
|
|
231
|
+
lines.push("Available Now:")
|
|
232
|
+
for (const id of graph.available) {
|
|
233
|
+
const t = todos.find(x => x.id === id)
|
|
234
|
+
const desc = t?.description?.trim() ? ` — ${t.description.trim()}` : ""
|
|
235
|
+
lines.push(` → ${PE(t?.priority)} ${id}: ${t?.content}${desc}`)
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
lines.push("Available Now: none")
|
|
239
|
+
}
|
|
240
|
+
lines.push("")
|
|
241
|
+
|
|
242
|
+
const multi = graph.parallel.filter(g => g.length > 1)
|
|
243
|
+
if (multi.length > 0) {
|
|
244
|
+
lines.push("Parallel Groups:")
|
|
245
|
+
multi.forEach((g, i) => lines.push(` Group ${i + 1}: ${g.join(", ")}`))
|
|
246
|
+
lines.push("")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Object.keys(graph.blocked).length > 0) {
|
|
250
|
+
lines.push("Blocked:")
|
|
251
|
+
for (const [id, blockers] of Object.entries(graph.blocked)) {
|
|
252
|
+
const t = todos.find(x => x.id === id)
|
|
253
|
+
const desc = t?.description?.trim() ? ` — ${t.description.trim()}` : ""
|
|
254
|
+
lines.push(` ⊗ ${id}: ${t?.content}${desc} ← waiting: ${blockers.join(", ")}`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return lines.join("\n")
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Tools
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
export const write = tool({
|
|
266
|
+
description: "Create or update TODO list. TODOv2 (Prefer this instead of TODO)",
|
|
267
|
+
args: {
|
|
268
|
+
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
|
+
})
|
|
277
|
+
).describe("Array of todos"),
|
|
278
|
+
},
|
|
279
|
+
async execute(args, context) {
|
|
280
|
+
const now = Date.now()
|
|
281
|
+
const todos = args.todos.map((t: any) => ({ ...t, createdAt: t.createdAt || now, updatedAt: now }))
|
|
282
|
+
await writeTodos(todos, context.sessionID, context.directory)
|
|
283
|
+
return formatGraph(analyzeGraph(todos))
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
export const read_next_five = tool({
|
|
288
|
+
description: "Read current TODO list. Shows Next 5 tasks.",
|
|
289
|
+
args: {},
|
|
290
|
+
async execute(_args, context) {
|
|
291
|
+
const todos = await readTodos(context.sessionID, context.directory)
|
|
292
|
+
const graph = analyzeGraph(todos)
|
|
293
|
+
if (graph.available.length === 0) return "No tasks available. All blocked or not ready."
|
|
294
|
+
const next5 = graph.available.slice(0, 5)
|
|
295
|
+
const lines: string[] = ["Next 5 available tasks:", ""]
|
|
296
|
+
for (const id of next5) {
|
|
297
|
+
const t = graph.todos.find(x => x.id === id)
|
|
298
|
+
if (t) {
|
|
299
|
+
const desc = t.description?.trim() ? `\n ${t.description.trim()}` : ""
|
|
300
|
+
lines.push(`${PE(t.priority)} ${id}: ${t.content}${desc}`)
|
|
301
|
+
lines.push("")
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (graph.available.length > 5) lines.push(`... +${graph.available.length - 5} more`)
|
|
305
|
+
return lines.join("\n")
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
export const read = tool({
|
|
310
|
+
description: "Read current TODO list. Shows all tasks.",
|
|
311
|
+
args: {},
|
|
312
|
+
async execute(_args, context) {
|
|
313
|
+
const todos = await readTodos(context.sessionID, context.directory)
|
|
314
|
+
if (todos.length === 0) return "No todos. Use usethis_todo_write to create."
|
|
315
|
+
return formatGraph(analyzeGraph(todos))
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
export const get_by_id = tool({
|
|
320
|
+
description: "Get one task by id.",
|
|
321
|
+
args: {
|
|
322
|
+
id: tool.schema.string().describe("Task ID"),
|
|
323
|
+
},
|
|
324
|
+
async execute(args, context) {
|
|
325
|
+
const todos = await readTodos(context.sessionID, context.directory)
|
|
326
|
+
const todo = todos.find(t => t.id === args.id)
|
|
327
|
+
if (!todo) return `❌ Task ${args.id} not found`
|
|
328
|
+
|
|
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")
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
export const update = tool({
|
|
343
|
+
description: "Update tasks. Same interface as write, but merges by id.",
|
|
344
|
+
args: {
|
|
345
|
+
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
|
+
})
|
|
354
|
+
).describe("Array of todos to update"),
|
|
355
|
+
},
|
|
356
|
+
async execute(args, context) {
|
|
357
|
+
const todos = await readTodos(context.sessionID, context.directory)
|
|
358
|
+
const now = Date.now()
|
|
359
|
+
const byId = new Map(todos.map(t => [t.id, t]))
|
|
360
|
+
|
|
361
|
+
for (const incoming of args.todos) {
|
|
362
|
+
const existing = byId.get(incoming.id)
|
|
363
|
+
if (existing) {
|
|
364
|
+
Object.assign(existing, incoming)
|
|
365
|
+
existing.updatedAt = now
|
|
366
|
+
} else {
|
|
367
|
+
byId.set(incoming.id, { ...incoming, createdAt: now, updatedAt: now })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const merged = [...byId.values()]
|
|
372
|
+
await writeTodos(merged, context.sessionID, context.directory)
|
|
373
|
+
return `✅ Updated ${args.todos.length} task(s)\n\n${formatGraph(analyzeGraph(merged))}`
|
|
374
|
+
},
|
|
375
|
+
})
|