@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 +1 -1
- package/src/build-info.json +2 -2
- package/src/opencode/FLOW.yaml +0 -284
- package/src/opencode/config.yaml +0 -10
- package/src/opencode/opencode.json +2 -1
- package/src/opencode/package.json +1 -1
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +102 -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 +344 -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
|
-
# }
|
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
|
# =============================================================================
|
|
@@ -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
|
+
})
|