trak_flow 0.1.3
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.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/CHANGELOG.md +69 -0
- data/COMMITS.md +196 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +281 -0
- data/README.md +479 -0
- data/Rakefile +16 -0
- data/bin/tf +6 -0
- data/bin/tf_mcp +81 -0
- data/docs/.keep +0 -0
- data/docs/api/database.md +434 -0
- data/docs/api/ruby-library.md +349 -0
- data/docs/api/task-model.md +341 -0
- data/docs/assets/stylesheets/extra.css +53 -0
- data/docs/assets/trak_flow.jpg +0 -0
- data/docs/cli/admin-commands.md +369 -0
- data/docs/cli/dependency-commands.md +321 -0
- data/docs/cli/label-commands.md +222 -0
- data/docs/cli/overview.md +163 -0
- data/docs/cli/plan-commands.md +344 -0
- data/docs/cli/task-commands.md +333 -0
- data/docs/core-concepts/dependencies.md +232 -0
- data/docs/core-concepts/labels.md +217 -0
- data/docs/core-concepts/overview.md +178 -0
- data/docs/core-concepts/plans-workflows.md +264 -0
- data/docs/core-concepts/tasks.md +205 -0
- data/docs/getting-started/configuration.md +120 -0
- data/docs/getting-started/installation.md +79 -0
- data/docs/getting-started/quick-start.md +245 -0
- data/docs/index.md +169 -0
- data/docs/mcp/integration.md +302 -0
- data/docs/mcp/overview.md +206 -0
- data/docs/mcp/resources.md +284 -0
- data/docs/mcp/tools.md +457 -0
- data/examples/basic_usage.rb +365 -0
- data/examples/cli_demo.sh +314 -0
- data/examples/mcp/Gemfile +9 -0
- data/examples/mcp/Gemfile.lock +226 -0
- data/examples/mcp/http_demo.rb +232 -0
- data/examples/mcp/stdio_demo.rb +146 -0
- data/lib/trak_flow/cli/admin_commands.rb +136 -0
- data/lib/trak_flow/cli/config_commands.rb +260 -0
- data/lib/trak_flow/cli/dep_commands.rb +71 -0
- data/lib/trak_flow/cli/label_commands.rb +76 -0
- data/lib/trak_flow/cli/main_commands.rb +386 -0
- data/lib/trak_flow/cli/plan_commands.rb +185 -0
- data/lib/trak_flow/cli/workflow_commands.rb +133 -0
- data/lib/trak_flow/cli.rb +110 -0
- data/lib/trak_flow/config/defaults.yml +114 -0
- data/lib/trak_flow/config/section.rb +74 -0
- data/lib/trak_flow/config.rb +276 -0
- data/lib/trak_flow/graph/dependency_graph.rb +288 -0
- data/lib/trak_flow/id_generator.rb +52 -0
- data/lib/trak_flow/mcp/resources/base_resource.rb +25 -0
- data/lib/trak_flow/mcp/resources/dependency_graph.rb +31 -0
- data/lib/trak_flow/mcp/resources/label_list.rb +21 -0
- data/lib/trak_flow/mcp/resources/plan_by_id.rb +27 -0
- data/lib/trak_flow/mcp/resources/plan_list.rb +21 -0
- data/lib/trak_flow/mcp/resources/task_by_id.rb +31 -0
- data/lib/trak_flow/mcp/resources/task_list.rb +21 -0
- data/lib/trak_flow/mcp/resources/task_next.rb +30 -0
- data/lib/trak_flow/mcp/resources/workflow_by_id.rb +27 -0
- data/lib/trak_flow/mcp/resources/workflow_list.rb +21 -0
- data/lib/trak_flow/mcp/server.rb +140 -0
- data/lib/trak_flow/mcp/tools/base_tool.rb +29 -0
- data/lib/trak_flow/mcp/tools/comment_add.rb +33 -0
- data/lib/trak_flow/mcp/tools/dep_add.rb +34 -0
- data/lib/trak_flow/mcp/tools/dep_remove.rb +25 -0
- data/lib/trak_flow/mcp/tools/label_add.rb +28 -0
- data/lib/trak_flow/mcp/tools/label_remove.rb +25 -0
- data/lib/trak_flow/mcp/tools/plan_add_step.rb +35 -0
- data/lib/trak_flow/mcp/tools/plan_create.rb +33 -0
- data/lib/trak_flow/mcp/tools/plan_run.rb +58 -0
- data/lib/trak_flow/mcp/tools/plan_start.rb +58 -0
- data/lib/trak_flow/mcp/tools/task_block.rb +27 -0
- data/lib/trak_flow/mcp/tools/task_close.rb +26 -0
- data/lib/trak_flow/mcp/tools/task_create.rb +51 -0
- data/lib/trak_flow/mcp/tools/task_defer.rb +27 -0
- data/lib/trak_flow/mcp/tools/task_start.rb +25 -0
- data/lib/trak_flow/mcp/tools/task_update.rb +36 -0
- data/lib/trak_flow/mcp/tools/workflow_discard.rb +28 -0
- data/lib/trak_flow/mcp/tools/workflow_summarize.rb +34 -0
- data/lib/trak_flow/mcp.rb +38 -0
- data/lib/trak_flow/models/comment.rb +71 -0
- data/lib/trak_flow/models/dependency.rb +96 -0
- data/lib/trak_flow/models/label.rb +90 -0
- data/lib/trak_flow/models/task.rb +188 -0
- data/lib/trak_flow/storage/database.rb +638 -0
- data/lib/trak_flow/storage/jsonl.rb +259 -0
- data/lib/trak_flow/time_parser.rb +15 -0
- data/lib/trak_flow/version.rb +5 -0
- data/lib/trak_flow.rb +100 -0
- data/mkdocs.yml +143 -0
- metadata +392 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fast_mcp"
|
|
4
|
+
require "puma"
|
|
5
|
+
require "puma/configuration"
|
|
6
|
+
require "rack"
|
|
7
|
+
require "rackup"
|
|
8
|
+
|
|
9
|
+
module TrakFlow
|
|
10
|
+
module Mcp
|
|
11
|
+
class Server
|
|
12
|
+
attr_reader :name, :version, :mcp_server
|
|
13
|
+
|
|
14
|
+
def initialize(name: "trak_flow", version: TrakFlow::VERSION)
|
|
15
|
+
@name = name
|
|
16
|
+
@version = version
|
|
17
|
+
@mcp_server = FastMcp::Server.new(name: name, version: version)
|
|
18
|
+
register_tools
|
|
19
|
+
register_resources
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start_stdio
|
|
23
|
+
# Use warn (stderr) not puts (stdout) - stdout is for MCP protocol
|
|
24
|
+
warn "Starting TrakFlow MCP Server (stdio transport)..."
|
|
25
|
+
mcp_server.start
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start_http(port: nil)
|
|
29
|
+
port ||= TrakFlow.config.mcp.port
|
|
30
|
+
puts "Starting TrakFlow MCP Server (HTTP transport on port #{port})..."
|
|
31
|
+
|
|
32
|
+
rack_app = create_rack_app
|
|
33
|
+
|
|
34
|
+
# Configure Puma (supports rack.hijack for SSE)
|
|
35
|
+
puma_config = Puma::Configuration.new do |config|
|
|
36
|
+
config.bind "tcp://0.0.0.0:#{port}"
|
|
37
|
+
config.threads 1, 5
|
|
38
|
+
config.workers 0
|
|
39
|
+
config.quiet
|
|
40
|
+
config.app rack_app
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
launcher = Puma::Launcher.new(puma_config)
|
|
44
|
+
|
|
45
|
+
trap("INT") { launcher.stop }
|
|
46
|
+
trap("TERM") { launcher.stop }
|
|
47
|
+
|
|
48
|
+
launcher.run
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def start_both(http_port: nil)
|
|
52
|
+
http_port ||= TrakFlow.config.mcp.port
|
|
53
|
+
puts "Starting TrakFlow MCP Server (dual transport)..."
|
|
54
|
+
puts " - HTTP: port #{http_port}"
|
|
55
|
+
puts " - STDIO: reading from stdin"
|
|
56
|
+
|
|
57
|
+
# Start HTTP in a thread
|
|
58
|
+
http_thread = Thread.new do
|
|
59
|
+
start_http_server(http_port)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Run STDIO in main thread (blocking)
|
|
63
|
+
mcp_server.start
|
|
64
|
+
|
|
65
|
+
http_thread.join
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def create_rack_app
|
|
71
|
+
server = mcp_server
|
|
72
|
+
Rack::Builder.new do
|
|
73
|
+
use FastMcp::Transports::RackTransport, server
|
|
74
|
+
run ->(_env) { [404, { "Content-Type" => "text/plain" }, ["TrakFlow MCP Server - Use /mcp/sse for SSE transport"]] }
|
|
75
|
+
end.to_app
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def start_http_server(port)
|
|
79
|
+
rack_app = create_rack_app
|
|
80
|
+
|
|
81
|
+
# Configure Puma (supports rack.hijack for SSE)
|
|
82
|
+
puma_config = Puma::Configuration.new do |config|
|
|
83
|
+
config.bind "tcp://0.0.0.0:#{port}"
|
|
84
|
+
config.threads 1, 5
|
|
85
|
+
config.workers 0
|
|
86
|
+
config.quiet
|
|
87
|
+
config.app rack_app
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
launcher = Puma::Launcher.new(puma_config)
|
|
91
|
+
|
|
92
|
+
trap("INT") { launcher.stop }
|
|
93
|
+
trap("TERM") { launcher.stop }
|
|
94
|
+
|
|
95
|
+
launcher.run
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def register_tools
|
|
99
|
+
# Task management tools
|
|
100
|
+
mcp_server.register_tool(Tools::TaskCreate)
|
|
101
|
+
mcp_server.register_tool(Tools::TaskUpdate)
|
|
102
|
+
mcp_server.register_tool(Tools::TaskClose)
|
|
103
|
+
mcp_server.register_tool(Tools::TaskStart)
|
|
104
|
+
mcp_server.register_tool(Tools::TaskBlock)
|
|
105
|
+
mcp_server.register_tool(Tools::TaskDefer)
|
|
106
|
+
|
|
107
|
+
# Plan/Workflow tools
|
|
108
|
+
mcp_server.register_tool(Tools::PlanCreate)
|
|
109
|
+
mcp_server.register_tool(Tools::PlanAddStep)
|
|
110
|
+
mcp_server.register_tool(Tools::PlanStart)
|
|
111
|
+
mcp_server.register_tool(Tools::PlanRun)
|
|
112
|
+
mcp_server.register_tool(Tools::WorkflowDiscard)
|
|
113
|
+
mcp_server.register_tool(Tools::WorkflowSummarize)
|
|
114
|
+
|
|
115
|
+
# Dependency tools
|
|
116
|
+
mcp_server.register_tool(Tools::DepAdd)
|
|
117
|
+
mcp_server.register_tool(Tools::DepRemove)
|
|
118
|
+
|
|
119
|
+
# Label tools
|
|
120
|
+
mcp_server.register_tool(Tools::LabelAdd)
|
|
121
|
+
mcp_server.register_tool(Tools::LabelRemove)
|
|
122
|
+
|
|
123
|
+
# Comment tool
|
|
124
|
+
mcp_server.register_tool(Tools::CommentAdd)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def register_resources
|
|
128
|
+
mcp_server.register_resource(Resources::TaskList)
|
|
129
|
+
mcp_server.register_resource(Resources::TaskById)
|
|
130
|
+
mcp_server.register_resource(Resources::TaskNext)
|
|
131
|
+
mcp_server.register_resource(Resources::PlanList)
|
|
132
|
+
mcp_server.register_resource(Resources::PlanById)
|
|
133
|
+
mcp_server.register_resource(Resources::WorkflowList)
|
|
134
|
+
mcp_server.register_resource(Resources::WorkflowById)
|
|
135
|
+
mcp_server.register_resource(Resources::LabelList)
|
|
136
|
+
mcp_server.register_resource(Resources::DependencyGraph)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class BaseTool < FastMcp::Tool
|
|
7
|
+
class << self
|
|
8
|
+
def with_database
|
|
9
|
+
TrakFlow.ensure_initialized!
|
|
10
|
+
|
|
11
|
+
db = Storage::Database.new
|
|
12
|
+
db.connect
|
|
13
|
+
|
|
14
|
+
jsonl = Storage::Jsonl.new
|
|
15
|
+
jsonl.import(db) if jsonl.exists?
|
|
16
|
+
|
|
17
|
+
result = yield db
|
|
18
|
+
|
|
19
|
+
jsonl.export(db) if db.dirty?
|
|
20
|
+
|
|
21
|
+
result
|
|
22
|
+
ensure
|
|
23
|
+
db&.close
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class CommentAdd < BaseTool
|
|
7
|
+
tool_name "comment_add"
|
|
8
|
+
description "Add a comment to a task"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:task_id).filled(:string).description("Task ID")
|
|
12
|
+
required(:body).filled(:string).description("Comment text")
|
|
13
|
+
optional(:author).filled(:string).description("Comment author (default: robot)")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(task_id:, body:, author: "robot")
|
|
17
|
+
self.class.with_database do |db|
|
|
18
|
+
db.find_task!(task_id)
|
|
19
|
+
|
|
20
|
+
comment = Models::Comment.new(
|
|
21
|
+
task_id: task_id,
|
|
22
|
+
body: body,
|
|
23
|
+
author: author
|
|
24
|
+
)
|
|
25
|
+
db.add_comment(comment)
|
|
26
|
+
|
|
27
|
+
comment.to_h
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class DepAdd < BaseTool
|
|
7
|
+
tool_name "dep_add"
|
|
8
|
+
description "Add a dependency between two tasks"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:source_id).filled(:string).description("Source task ID")
|
|
12
|
+
required(:target_id).filled(:string).description("Target task ID")
|
|
13
|
+
optional(:type).filled(:string).description("Dependency type: blocks, related, parent-child, discovered-from (default: blocks)")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(source_id:, target_id:, type: "blocks")
|
|
17
|
+
self.class.with_database do |db|
|
|
18
|
+
db.find_task!(source_id)
|
|
19
|
+
db.find_task!(target_id)
|
|
20
|
+
|
|
21
|
+
dep = Models::Dependency.new(
|
|
22
|
+
source_id: source_id,
|
|
23
|
+
target_id: target_id,
|
|
24
|
+
type: type
|
|
25
|
+
)
|
|
26
|
+
db.add_dependency(dep)
|
|
27
|
+
|
|
28
|
+
{ source_id: source_id, target_id: target_id, type: type, success: true }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class DepRemove < BaseTool
|
|
7
|
+
tool_name "dep_remove"
|
|
8
|
+
description "Remove a dependency between two tasks"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:source_id).filled(:string).description("Source task ID")
|
|
12
|
+
required(:target_id).filled(:string).description("Target task ID")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(source_id:, target_id:)
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
db.remove_dependency(source_id, target_id)
|
|
18
|
+
|
|
19
|
+
{ source_id: source_id, target_id: target_id, removed: true }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class LabelAdd < BaseTool
|
|
7
|
+
tool_name "label_add"
|
|
8
|
+
description "Add a label to a task"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:task_id).filled(:string).description("Task ID")
|
|
12
|
+
required(:name).filled(:string).description("Label name")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(task_id:, name:)
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
db.find_task!(task_id)
|
|
18
|
+
|
|
19
|
+
label = Models::Label.new(task_id: task_id, name: name)
|
|
20
|
+
db.add_label(label)
|
|
21
|
+
|
|
22
|
+
{ task_id: task_id, label: name, success: true }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class LabelRemove < BaseTool
|
|
7
|
+
tool_name "label_remove"
|
|
8
|
+
description "Remove a label from a task"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:task_id).filled(:string).description("Task ID")
|
|
12
|
+
required(:name).filled(:string).description("Label name")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(task_id:, name:)
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
db.remove_label(task_id, name)
|
|
18
|
+
|
|
19
|
+
{ task_id: task_id, label: name, removed: true }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class PlanAddStep < BaseTool
|
|
7
|
+
tool_name "plan_add_step"
|
|
8
|
+
description "Add a step (child task) to a Plan"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:plan_id).filled(:string).description("Plan ID to add step to")
|
|
12
|
+
required(:title).filled(:string).description("Step title")
|
|
13
|
+
optional(:description).filled(:string).description("Step description")
|
|
14
|
+
optional(:type).filled(:string).description("Task type (default: task)")
|
|
15
|
+
optional(:priority).filled(:integer).description("Priority 0-4 (default: 2)")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(plan_id:, title:, description: nil, type: "task", priority: 2)
|
|
19
|
+
self.class.with_database do |db|
|
|
20
|
+
plan = db.find_task!(plan_id)
|
|
21
|
+
raise TrakFlow::Error, "#{plan_id} is not a Plan" unless plan.plan?
|
|
22
|
+
|
|
23
|
+
task = db.create_child_task(plan_id, {
|
|
24
|
+
title: title,
|
|
25
|
+
description: description || "",
|
|
26
|
+
type: type,
|
|
27
|
+
priority: priority
|
|
28
|
+
})
|
|
29
|
+
task.to_h
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class PlanCreate < BaseTool
|
|
7
|
+
tool_name "plan_create"
|
|
8
|
+
description "Create a new Plan (workflow blueprint)"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:title).filled(:string).description("Plan title")
|
|
12
|
+
optional(:description).filled(:string).description("Plan description")
|
|
13
|
+
optional(:type).filled(:string).description("Task type (default: task)")
|
|
14
|
+
optional(:priority).filled(:integer).description("Priority 0-4 (default: 2)")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(title:, description: nil, type: "task", priority: 2)
|
|
18
|
+
self.class.with_database do |db|
|
|
19
|
+
plan = Models::Task.new(
|
|
20
|
+
title: title,
|
|
21
|
+
description: description || "",
|
|
22
|
+
type: type,
|
|
23
|
+
priority: priority,
|
|
24
|
+
plan: true
|
|
25
|
+
)
|
|
26
|
+
db.create_task(plan)
|
|
27
|
+
plan.to_h
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class PlanRun < BaseTool
|
|
7
|
+
tool_name "plan_run"
|
|
8
|
+
description "Create an ephemeral Workflow from a Plan (one-shot, garbage collectible)"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:plan_id).filled(:string).description("Plan ID to instantiate")
|
|
12
|
+
optional(:variables).hash.description("Template variables for interpolation")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(plan_id:, variables: {})
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
plan = db.find_task!(plan_id)
|
|
18
|
+
raise TrakFlow::Error, "#{plan_id} is not a Plan" unless plan.plan?
|
|
19
|
+
|
|
20
|
+
workflow = Models::Task.new(
|
|
21
|
+
title: interpolate_vars(plan.title, variables),
|
|
22
|
+
description: interpolate_vars(plan.description, variables),
|
|
23
|
+
type: plan.type,
|
|
24
|
+
priority: plan.priority,
|
|
25
|
+
source_plan_id: plan.id,
|
|
26
|
+
ephemeral: true
|
|
27
|
+
)
|
|
28
|
+
db.create_task(workflow)
|
|
29
|
+
workflow.append_trace("INSTANTIATED", "from Plan #{plan.id} (ephemeral)")
|
|
30
|
+
db.update_task(workflow)
|
|
31
|
+
|
|
32
|
+
db.find_plan_tasks(plan_id).each do |step|
|
|
33
|
+
db.create_child_task(workflow.id, {
|
|
34
|
+
title: interpolate_vars(step.title, variables),
|
|
35
|
+
description: interpolate_vars(step.description, variables),
|
|
36
|
+
type: step.type,
|
|
37
|
+
priority: step.priority,
|
|
38
|
+
ephemeral: true
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
workflow.to_h
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def interpolate_vars(text, vars)
|
|
49
|
+
return text if text.nil? || vars.empty?
|
|
50
|
+
|
|
51
|
+
result = text.dup
|
|
52
|
+
vars.each { |key, value| result.gsub!("{{#{key}}}", value.to_s) }
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class PlanStart < BaseTool
|
|
7
|
+
tool_name "plan_start"
|
|
8
|
+
description "Create a persistent Workflow from a Plan"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:plan_id).filled(:string).description("Plan ID to instantiate")
|
|
12
|
+
optional(:variables).hash.description("Template variables for interpolation")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(plan_id:, variables: {})
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
plan = db.find_task!(plan_id)
|
|
18
|
+
raise TrakFlow::Error, "#{plan_id} is not a Plan" unless plan.plan?
|
|
19
|
+
|
|
20
|
+
workflow = Models::Task.new(
|
|
21
|
+
title: interpolate_vars(plan.title, variables),
|
|
22
|
+
description: interpolate_vars(plan.description, variables),
|
|
23
|
+
type: plan.type,
|
|
24
|
+
priority: plan.priority,
|
|
25
|
+
source_plan_id: plan.id,
|
|
26
|
+
ephemeral: false
|
|
27
|
+
)
|
|
28
|
+
db.create_task(workflow)
|
|
29
|
+
workflow.append_trace("INSTANTIATED", "from Plan #{plan.id}")
|
|
30
|
+
db.update_task(workflow)
|
|
31
|
+
|
|
32
|
+
db.find_plan_tasks(plan_id).each do |step|
|
|
33
|
+
db.create_child_task(workflow.id, {
|
|
34
|
+
title: interpolate_vars(step.title, variables),
|
|
35
|
+
description: interpolate_vars(step.description, variables),
|
|
36
|
+
type: step.type,
|
|
37
|
+
priority: step.priority,
|
|
38
|
+
ephemeral: false
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
workflow.to_h
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def interpolate_vars(text, vars)
|
|
49
|
+
return text if text.nil? || vars.empty?
|
|
50
|
+
|
|
51
|
+
result = text.dup
|
|
52
|
+
vars.each { |key, value| result.gsub!("{{#{key}}}", value.to_s) }
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class TaskBlock < BaseTool
|
|
7
|
+
tool_name "task_block"
|
|
8
|
+
description "Mark a task as blocked"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:id).filled(:string).description("Task ID to block")
|
|
12
|
+
optional(:reason).filled(:string).description("Reason for blocking")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(id:, reason: nil)
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
task = db.find_task!(id)
|
|
18
|
+
task.status = "blocked"
|
|
19
|
+
task.append_trace("BLOCKED", reason) if reason
|
|
20
|
+
db.update_task(task)
|
|
21
|
+
task.to_h
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class TaskClose < BaseTool
|
|
7
|
+
tool_name "task_close"
|
|
8
|
+
description "Close a task"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:id).filled(:string).description("Task ID to close")
|
|
12
|
+
optional(:reason).filled(:string).description("Reason for closing")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(id:, reason: nil)
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
task = db.find_task!(id)
|
|
18
|
+
task.close!(reason: reason)
|
|
19
|
+
db.update_task(task)
|
|
20
|
+
task.to_h
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class TaskCreate < BaseTool
|
|
7
|
+
tool_name "task_create"
|
|
8
|
+
description "Create a new task with title, type, and priority"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:title).filled(:string).description("Task title")
|
|
12
|
+
optional(:type).filled(:string).description("Task type: bug, feature, task, epic, chore (default: task)")
|
|
13
|
+
optional(:priority).filled(:integer).description("Priority 0-4: 0=critical, 4=backlog (default: 2)")
|
|
14
|
+
optional(:description).filled(:string).description("Task description")
|
|
15
|
+
optional(:assignee).filled(:string).description("Assignee name")
|
|
16
|
+
optional(:parent_id).filled(:string).description("Parent task ID for creating child tasks")
|
|
17
|
+
optional(:labels).array(:string).description("Labels to add")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(title:, type: "task", priority: 2, description: nil, assignee: nil, parent_id: nil, labels: [])
|
|
21
|
+
self.class.with_database do |db|
|
|
22
|
+
task = if parent_id
|
|
23
|
+
db.create_child_task(parent_id, {
|
|
24
|
+
title: title,
|
|
25
|
+
description: description,
|
|
26
|
+
type: type,
|
|
27
|
+
priority: priority,
|
|
28
|
+
assignee: assignee
|
|
29
|
+
})
|
|
30
|
+
else
|
|
31
|
+
new_task = Models::Task.new(
|
|
32
|
+
title: title,
|
|
33
|
+
description: description,
|
|
34
|
+
type: type,
|
|
35
|
+
priority: priority,
|
|
36
|
+
assignee: assignee
|
|
37
|
+
)
|
|
38
|
+
db.create_task(new_task)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
labels.each do |label_name|
|
|
42
|
+
db.add_label(Models::Label.new(task_id: task.id, name: label_name))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
task.to_h
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class TaskDefer < BaseTool
|
|
7
|
+
tool_name "task_defer"
|
|
8
|
+
description "Defer a task for later"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:id).filled(:string).description("Task ID to defer")
|
|
12
|
+
optional(:reason).filled(:string).description("Reason for deferring")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(id:, reason: nil)
|
|
16
|
+
self.class.with_database do |db|
|
|
17
|
+
task = db.find_task!(id)
|
|
18
|
+
task.status = "deferred"
|
|
19
|
+
task.append_trace("DEFERRED", reason) if reason
|
|
20
|
+
db.update_task(task)
|
|
21
|
+
task.to_h
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TrakFlow
|
|
4
|
+
module Mcp
|
|
5
|
+
module Tools
|
|
6
|
+
class TaskStart < BaseTool
|
|
7
|
+
tool_name "task_start"
|
|
8
|
+
description "Start working on a task (set status to in_progress)"
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:id).filled(:string).description("Task ID to start")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(id:)
|
|
15
|
+
self.class.with_database do |db|
|
|
16
|
+
task = db.find_task!(id)
|
|
17
|
+
task.status = "in_progress"
|
|
18
|
+
db.update_task(task)
|
|
19
|
+
task.to_h
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|