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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/CHANGELOG.md +69 -0
  4. data/COMMITS.md +196 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +281 -0
  7. data/README.md +479 -0
  8. data/Rakefile +16 -0
  9. data/bin/tf +6 -0
  10. data/bin/tf_mcp +81 -0
  11. data/docs/.keep +0 -0
  12. data/docs/api/database.md +434 -0
  13. data/docs/api/ruby-library.md +349 -0
  14. data/docs/api/task-model.md +341 -0
  15. data/docs/assets/stylesheets/extra.css +53 -0
  16. data/docs/assets/trak_flow.jpg +0 -0
  17. data/docs/cli/admin-commands.md +369 -0
  18. data/docs/cli/dependency-commands.md +321 -0
  19. data/docs/cli/label-commands.md +222 -0
  20. data/docs/cli/overview.md +163 -0
  21. data/docs/cli/plan-commands.md +344 -0
  22. data/docs/cli/task-commands.md +333 -0
  23. data/docs/core-concepts/dependencies.md +232 -0
  24. data/docs/core-concepts/labels.md +217 -0
  25. data/docs/core-concepts/overview.md +178 -0
  26. data/docs/core-concepts/plans-workflows.md +264 -0
  27. data/docs/core-concepts/tasks.md +205 -0
  28. data/docs/getting-started/configuration.md +120 -0
  29. data/docs/getting-started/installation.md +79 -0
  30. data/docs/getting-started/quick-start.md +245 -0
  31. data/docs/index.md +169 -0
  32. data/docs/mcp/integration.md +302 -0
  33. data/docs/mcp/overview.md +206 -0
  34. data/docs/mcp/resources.md +284 -0
  35. data/docs/mcp/tools.md +457 -0
  36. data/examples/basic_usage.rb +365 -0
  37. data/examples/cli_demo.sh +314 -0
  38. data/examples/mcp/Gemfile +9 -0
  39. data/examples/mcp/Gemfile.lock +226 -0
  40. data/examples/mcp/http_demo.rb +232 -0
  41. data/examples/mcp/stdio_demo.rb +146 -0
  42. data/lib/trak_flow/cli/admin_commands.rb +136 -0
  43. data/lib/trak_flow/cli/config_commands.rb +260 -0
  44. data/lib/trak_flow/cli/dep_commands.rb +71 -0
  45. data/lib/trak_flow/cli/label_commands.rb +76 -0
  46. data/lib/trak_flow/cli/main_commands.rb +386 -0
  47. data/lib/trak_flow/cli/plan_commands.rb +185 -0
  48. data/lib/trak_flow/cli/workflow_commands.rb +133 -0
  49. data/lib/trak_flow/cli.rb +110 -0
  50. data/lib/trak_flow/config/defaults.yml +114 -0
  51. data/lib/trak_flow/config/section.rb +74 -0
  52. data/lib/trak_flow/config.rb +276 -0
  53. data/lib/trak_flow/graph/dependency_graph.rb +288 -0
  54. data/lib/trak_flow/id_generator.rb +52 -0
  55. data/lib/trak_flow/mcp/resources/base_resource.rb +25 -0
  56. data/lib/trak_flow/mcp/resources/dependency_graph.rb +31 -0
  57. data/lib/trak_flow/mcp/resources/label_list.rb +21 -0
  58. data/lib/trak_flow/mcp/resources/plan_by_id.rb +27 -0
  59. data/lib/trak_flow/mcp/resources/plan_list.rb +21 -0
  60. data/lib/trak_flow/mcp/resources/task_by_id.rb +31 -0
  61. data/lib/trak_flow/mcp/resources/task_list.rb +21 -0
  62. data/lib/trak_flow/mcp/resources/task_next.rb +30 -0
  63. data/lib/trak_flow/mcp/resources/workflow_by_id.rb +27 -0
  64. data/lib/trak_flow/mcp/resources/workflow_list.rb +21 -0
  65. data/lib/trak_flow/mcp/server.rb +140 -0
  66. data/lib/trak_flow/mcp/tools/base_tool.rb +29 -0
  67. data/lib/trak_flow/mcp/tools/comment_add.rb +33 -0
  68. data/lib/trak_flow/mcp/tools/dep_add.rb +34 -0
  69. data/lib/trak_flow/mcp/tools/dep_remove.rb +25 -0
  70. data/lib/trak_flow/mcp/tools/label_add.rb +28 -0
  71. data/lib/trak_flow/mcp/tools/label_remove.rb +25 -0
  72. data/lib/trak_flow/mcp/tools/plan_add_step.rb +35 -0
  73. data/lib/trak_flow/mcp/tools/plan_create.rb +33 -0
  74. data/lib/trak_flow/mcp/tools/plan_run.rb +58 -0
  75. data/lib/trak_flow/mcp/tools/plan_start.rb +58 -0
  76. data/lib/trak_flow/mcp/tools/task_block.rb +27 -0
  77. data/lib/trak_flow/mcp/tools/task_close.rb +26 -0
  78. data/lib/trak_flow/mcp/tools/task_create.rb +51 -0
  79. data/lib/trak_flow/mcp/tools/task_defer.rb +27 -0
  80. data/lib/trak_flow/mcp/tools/task_start.rb +25 -0
  81. data/lib/trak_flow/mcp/tools/task_update.rb +36 -0
  82. data/lib/trak_flow/mcp/tools/workflow_discard.rb +28 -0
  83. data/lib/trak_flow/mcp/tools/workflow_summarize.rb +34 -0
  84. data/lib/trak_flow/mcp.rb +38 -0
  85. data/lib/trak_flow/models/comment.rb +71 -0
  86. data/lib/trak_flow/models/dependency.rb +96 -0
  87. data/lib/trak_flow/models/label.rb +90 -0
  88. data/lib/trak_flow/models/task.rb +188 -0
  89. data/lib/trak_flow/storage/database.rb +638 -0
  90. data/lib/trak_flow/storage/jsonl.rb +259 -0
  91. data/lib/trak_flow/time_parser.rb +15 -0
  92. data/lib/trak_flow/version.rb +5 -0
  93. data/lib/trak_flow.rb +100 -0
  94. data/mkdocs.yml +143 -0
  95. metadata +392 -0
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Storage
5
+ # JSONL (JSON Lines) persistence layer for Git integration
6
+ # This is the git-tracked source of truth stored in .trak_flow/tasks.jsonl
7
+ # One JSON entity per line makes diffs readable and merges usually automatic
8
+ class Jsonl
9
+ ENTITY_TYPES = %w[task dependency label comment].freeze
10
+
11
+ attr_reader :path
12
+
13
+ def initialize(path = nil)
14
+ @path = path || TrakFlow.jsonl_path
15
+ end
16
+
17
+ # Export all data from database to JSONL file
18
+ # - Plans are exported (persistent blueprints)
19
+ # - Ephemeral Workflows are NOT exported (temporary only)
20
+ def export(db)
21
+ entities = []
22
+
23
+ # Export regular tasks (excluding ephemeral) and include Plans
24
+ db.list_tasks(include_ephemeral: false, include_plans: true, include_tombstones: true).each do |task|
25
+ entities << { type: "task", data: task.to_h }
26
+ end
27
+
28
+ db.all_task_ids.each do |task_id|
29
+ db.find_dependencies(task_id, direction: :outgoing).each do |dep|
30
+ entities << { type: "dependency", data: dep.to_h }
31
+ end
32
+
33
+ db.find_labels(task_id).each do |label|
34
+ entities << { type: "label", data: label.to_h }
35
+ end
36
+
37
+ db.find_comments(task_id).each do |comment|
38
+ entities << { type: "comment", data: comment.to_h }
39
+ end
40
+ end
41
+
42
+ write_entities(entities)
43
+ db.mark_clean!
44
+ end
45
+
46
+ # Import all data from JSONL file to database
47
+ # @param db [Database] the database to import into
48
+ # @param orphan_handling [String] how to handle orphaned tasks
49
+ # @param error_policy [String] how to handle import errors: "warn", "strict", or "ignore"
50
+ def import(db, orphan_handling: nil, error_policy: nil)
51
+ orphan_handling ||= TrakFlow.config.get("import.orphan_handling")
52
+ error_policy ||= TrakFlow.config.get("import.error_policy") || "warn"
53
+
54
+ entities = read_entities
55
+ tasks = []
56
+ dependencies = []
57
+ labels = []
58
+ comments = []
59
+ import_errors = []
60
+
61
+ entities.each do |entity|
62
+ case entity[:type]
63
+ when "task"
64
+ tasks << Models::Task.from_hash(entity[:data])
65
+ when "dependency"
66
+ dependencies << Models::Dependency.from_hash(entity[:data])
67
+ when "label"
68
+ labels << Models::Label.from_hash(entity[:data])
69
+ when "comment"
70
+ comments << Models::Comment.from_hash(entity[:data])
71
+ end
72
+ end
73
+
74
+ tasks = handle_orphans(tasks, orphan_handling)
75
+
76
+ db.import_tasks(tasks)
77
+
78
+ import_errors += import_entities(db, :add_dependency, dependencies, error_policy)
79
+ import_errors += import_entities(db, :add_label, labels, error_policy)
80
+ import_errors += import_entities(db, :add_comment, comments, error_policy)
81
+
82
+ raise_if_strict_errors(import_errors, error_policy)
83
+ end
84
+
85
+ private
86
+
87
+ def import_entities(db, method, entities, error_policy)
88
+ errors = []
89
+ entities.each do |entity|
90
+ db.send(method, entity)
91
+ rescue Error => e
92
+ error_info = { entity_type: entity.class.name, error: e.message }
93
+ errors << error_info
94
+ handle_import_error(error_info, error_policy)
95
+ end
96
+ errors
97
+ end
98
+
99
+ def handle_import_error(error_info, policy)
100
+ case policy
101
+ when "strict"
102
+ # Errors collected for batch raise
103
+ when "warn"
104
+ debug_me "Warning: Import failed for #{error_info[:entity_type]}: #{error_info[:error]}"
105
+ when "ignore"
106
+ # Silent
107
+ end
108
+ end
109
+
110
+ def raise_if_strict_errors(errors, policy)
111
+ return if errors.empty? || policy != "strict"
112
+
113
+ messages = errors.map { |e| "#{e[:entity_type]}: #{e[:error]}" }
114
+ raise ValidationError, "Import failed with #{errors.size} error(s):\n #{messages.join("\n ")}"
115
+ end
116
+
117
+ public
118
+
119
+ # Check if JSONL file has changed since last import
120
+ def changed_since?(timestamp)
121
+ return true unless File.exist?(path)
122
+
123
+ File.mtime(path) > timestamp
124
+ end
125
+
126
+ # Get content hash of the JSONL file
127
+ def content_hash
128
+ return nil unless File.exist?(path)
129
+
130
+ Digest::SHA256.hexdigest(File.read(path))[0, 16]
131
+ end
132
+
133
+ # Check if file exists
134
+ def exists?
135
+ File.exist?(path)
136
+ end
137
+
138
+ # Read raw entities from file
139
+ def read_entities
140
+ return [] unless File.exist?(path)
141
+
142
+ entities = []
143
+ File.readlines(path).each_with_index do |line, index|
144
+ line = line.strip
145
+ next if line.empty? || line.start_with?("#")
146
+
147
+ begin
148
+ data = Oj.load(line, mode: :compat, symbol_keys: true)
149
+ entities << data if valid_entity?(data)
150
+ rescue Oj::ParseError => e
151
+ debug_me "Warning: Could not parse line #{index + 1}: #{e.message}"
152
+ end
153
+ end
154
+
155
+ entities
156
+ end
157
+
158
+ # Write entities to file
159
+ def write_entities(entities)
160
+ FileUtils.mkdir_p(File.dirname(path))
161
+
162
+ File.open(path, "w") do |f|
163
+ f.puts "# TrakFlow task tracker data"
164
+ f.puts "# Generated at #{Time.now.utc.iso8601}"
165
+ f.puts ""
166
+
167
+ entities.each do |entity|
168
+ f.puts Oj.dump(entity, mode: :compat)
169
+ end
170
+ end
171
+ end
172
+
173
+ # Incremental export - only export changed entities
174
+ def incremental_export(db, changed_ids)
175
+ return export(db) unless File.exist?(path)
176
+
177
+ existing = read_entities
178
+ existing_by_id = {}
179
+
180
+ existing.each do |entity|
181
+ id = entity.dig(:data, :id)
182
+ existing_by_id["#{entity[:type]}-#{id}"] = entity if id
183
+ end
184
+
185
+ changed_ids.each do |task_id|
186
+ task = db.find_task(task_id)
187
+ if task
188
+ key = "task-#{task_id}"
189
+ existing_by_id[key] = { type: "task", data: task.to_h }
190
+
191
+ db.find_dependencies(task_id, direction: :outgoing).each do |dep|
192
+ dep_key = "dependency-#{dep.id}"
193
+ existing_by_id[dep_key] = { type: "dependency", data: dep.to_h }
194
+ end
195
+
196
+ db.find_labels(task_id).each do |label|
197
+ label_key = "label-#{label.id}"
198
+ existing_by_id[label_key] = { type: "label", data: label.to_h }
199
+ end
200
+
201
+ db.find_comments(task_id).each do |comment|
202
+ comment_key = "comment-#{comment.id}"
203
+ existing_by_id[comment_key] = { type: "comment", data: comment.to_h }
204
+ end
205
+ else
206
+ existing_by_id.delete("task-#{task_id}")
207
+ end
208
+ end
209
+
210
+ write_entities(existing_by_id.values)
211
+ db.mark_clean!
212
+ end
213
+
214
+ private
215
+
216
+ def valid_entity?(data)
217
+ return false unless data.is_a?(Hash)
218
+ return false unless ENTITY_TYPES.include?(data[:type])
219
+ return false unless data[:data].is_a?(Hash)
220
+
221
+ true
222
+ end
223
+
224
+ def handle_orphans(tasks, handling)
225
+ task_ids = Set.new(tasks.map(&:id))
226
+ orphans = []
227
+ valid = []
228
+
229
+ tasks.each do |task|
230
+ if task.parent_id && !task_ids.include?(task.parent_id)
231
+ orphans << task
232
+ else
233
+ valid << task
234
+ end
235
+ end
236
+
237
+ return valid if orphans.empty?
238
+
239
+ case handling
240
+ when "allow"
241
+ valid + orphans
242
+ when "skip"
243
+ debug_me "Skipping #{orphans.size} orphaned tasks"
244
+ valid
245
+ when "resurrect"
246
+ orphans.each do |orphan|
247
+ debug_me "Resurrecting orphan: #{orphan.id} (parent: #{orphan.parent_id})"
248
+ orphan.parent_id = nil
249
+ end
250
+ valid + orphans
251
+ when "strict"
252
+ raise ValidationError, "Found #{orphans.size} orphaned tasks with missing parents"
253
+ else
254
+ valid + orphans
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ # Shared time parsing utilities for model deserialization
5
+ module TimeParser
6
+ def self.parse(value)
7
+ return nil if value.nil?
8
+ return value if value.is_a?(Time)
9
+
10
+ Time.parse(value)
11
+ rescue ArgumentError
12
+ nil
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ VERSION = '0.1.3'
5
+ end
data/lib/trak_flow.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "securerandom"
7
+ require "set"
8
+ require "time"
9
+
10
+ require "oj"
11
+ require "sequel"
12
+ require "sqlite3"
13
+ require "thor"
14
+ require "pastel"
15
+ require "tty-table"
16
+ require "debug_me"
17
+ require "anyway_config"
18
+
19
+ module TrakFlow
20
+ class Error < StandardError; end
21
+ class NotInitializedError < Error; end
22
+ class TaskNotFoundError < Error; end
23
+ class DependencyCycleError < Error; end
24
+ class ValidationError < Error; end
25
+ class ConfigurationError < Error; end
26
+
27
+ TRAK_FLOW_DIR = ".trak_flow"
28
+ DATABASE_FILE = "trak_flow.db"
29
+ JSONL_FILE = "tasks.jsonl" # Default, can be overridden via config.storage.jsonl_file
30
+ CONFIG_FILE = "config.yml"
31
+
32
+ STATUSES = %w[open in_progress blocked deferred closed tombstone pinned].freeze
33
+ PRIORITIES = (0..4).to_a.freeze
34
+ TYPES = %w[bug feature task epic chore].freeze
35
+ DEPENDENCY_TYPES = %w[blocks related parent-child discovered-from].freeze
36
+
37
+ class << self
38
+ def root
39
+ @root ||= find_root
40
+ end
41
+
42
+ def trak_flow_dir
43
+ File.join(root, TRAK_FLOW_DIR)
44
+ end
45
+
46
+ def database_path
47
+ path = config.database.path
48
+ File.expand_path(path)
49
+ end
50
+
51
+ def jsonl_path
52
+ jsonl_file = config.storage.jsonl_file rescue JSONL_FILE
53
+ File.join(trak_flow_dir, jsonl_file)
54
+ end
55
+
56
+ def config_path
57
+ File.join(trak_flow_dir, CONFIG_FILE)
58
+ end
59
+
60
+ def initialized?
61
+ File.directory?(trak_flow_dir) && File.exist?(database_path)
62
+ end
63
+
64
+ def ensure_initialized!
65
+ raise NotInitializedError, "TrakFlow not initialized. Run 'tf init' first." unless initialized?
66
+ end
67
+
68
+ def reset_root!
69
+ @root = nil
70
+ end
71
+
72
+ private
73
+
74
+ def find_root(start_dir = Dir.pwd)
75
+ dir = start_dir
76
+ loop do
77
+ trak_flow_path = File.join(dir, TRAK_FLOW_DIR)
78
+ return dir if File.directory?(trak_flow_path)
79
+
80
+ parent = File.dirname(dir)
81
+ return start_dir if parent == dir
82
+
83
+ dir = parent
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ require_relative "trak_flow/version"
90
+ require_relative "trak_flow/id_generator"
91
+ require_relative "trak_flow/time_parser"
92
+ require_relative "trak_flow/config"
93
+ require_relative "trak_flow/models/task"
94
+ require_relative "trak_flow/models/dependency"
95
+ require_relative "trak_flow/models/label"
96
+ require_relative "trak_flow/models/comment"
97
+ require_relative "trak_flow/storage/database"
98
+ require_relative "trak_flow/storage/jsonl"
99
+ require_relative "trak_flow/graph/dependency_graph"
100
+ require_relative "trak_flow/cli"
data/mkdocs.yml ADDED
@@ -0,0 +1,143 @@
1
+ site_name: TrakFlow Documentation
2
+ site_description: "A distributed task tracking system for AI agents with DAG-based workflow engine"
3
+ site_author: "Dewayne VanHoozer"
4
+ site_url: "https://madbomber.github.io/trak_flow/"
5
+
6
+ repo_name: "MadBomber/trak_flow"
7
+ repo_url: "https://github.com/MadBomber/trak_flow"
8
+ edit_uri: "edit/main/docs/"
9
+
10
+ theme:
11
+ name: material
12
+ language: en
13
+ favicon: assets/logo.png
14
+
15
+ palette:
16
+ - media: "(prefers-color-scheme: light)"
17
+ scheme: default
18
+ primary: teal
19
+ accent: deep-orange
20
+ toggle:
21
+ icon: material/brightness-7
22
+ name: Switch to dark mode
23
+
24
+ - media: "(prefers-color-scheme: dark)"
25
+ scheme: slate
26
+ primary: teal
27
+ accent: deep-orange
28
+ toggle:
29
+ icon: material/brightness-4
30
+ name: Switch to light mode
31
+
32
+ font:
33
+ text: Roboto
34
+ code: Roboto Mono
35
+
36
+ features:
37
+ - navigation.instant
38
+ - navigation.tracking
39
+ - navigation.tabs
40
+ - navigation.tabs.sticky
41
+ - navigation.sections
42
+ - navigation.path
43
+ - navigation.indexes
44
+ - navigation.top
45
+ - toc.follow
46
+ - search.suggest
47
+ - search.highlight
48
+ - search.share
49
+ - header.autohide
50
+ - content.code.copy
51
+ - content.code.annotate
52
+ - content.tabs.link
53
+ - content.tooltips
54
+ - content.action.edit
55
+ - content.action.view
56
+
57
+ plugins:
58
+ - search:
59
+ separator: '[\s\-,:!=\[\]()"\`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
60
+
61
+ markdown_extensions:
62
+ - abbr
63
+ - admonition
64
+ - attr_list
65
+ - def_list
66
+ - footnotes
67
+ - md_in_html
68
+ - toc:
69
+ permalink: true
70
+ title: On this page
71
+ - pymdownx.arithmatex:
72
+ generic: true
73
+ - pymdownx.betterem:
74
+ smart_enable: all
75
+ - pymdownx.caret
76
+ - pymdownx.details
77
+ - pymdownx.emoji:
78
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
79
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
80
+ - pymdownx.highlight:
81
+ anchor_linenums: true
82
+ line_spans: __span
83
+ pygments_lang_class: true
84
+ - pymdownx.inlinehilite
85
+ - pymdownx.keys
86
+ - pymdownx.magiclink:
87
+ repo_url_shorthand: true
88
+ user: MadBomber
89
+ repo: trak_flow
90
+ - pymdownx.mark
91
+ - pymdownx.smartsymbols
92
+ - pymdownx.superfences:
93
+ custom_fences:
94
+ - name: mermaid
95
+ class: mermaid
96
+ format: !!python/name:pymdownx.superfences.fence_code_format
97
+ - pymdownx.tabbed:
98
+ alternate_style: true
99
+ - pymdownx.tasklist:
100
+ custom_checkbox: true
101
+ - pymdownx.tilde
102
+
103
+ extra_css:
104
+ - assets/stylesheets/extra.css
105
+
106
+ extra:
107
+ version: 0.0.1
108
+ social:
109
+ - icon: fontawesome/brands/github
110
+ link: https://github.com/MadBomber/trak_flow
111
+ name: GitHub Repository
112
+ generator: false
113
+
114
+ nav:
115
+ - Home: index.md
116
+ - Getting Started:
117
+ - Installation: getting-started/installation.md
118
+ - Quick Start: getting-started/quick-start.md
119
+ - Configuration: getting-started/configuration.md
120
+ - Core Concepts:
121
+ - Overview: core-concepts/overview.md
122
+ - Tasks: core-concepts/tasks.md
123
+ - Plans & Workflows: core-concepts/plans-workflows.md
124
+ - Dependencies: core-concepts/dependencies.md
125
+ - Labels: core-concepts/labels.md
126
+ - CLI Reference:
127
+ - Overview: cli/overview.md
128
+ - Task Commands: cli/task-commands.md
129
+ - Plan & Workflow Commands: cli/plan-commands.md
130
+ - Dependency Commands: cli/dependency-commands.md
131
+ - Label Commands: cli/label-commands.md
132
+ - Admin Commands: cli/admin-commands.md
133
+ - MCP Server:
134
+ - Overview: mcp/overview.md
135
+ - Tools Reference: mcp/tools.md
136
+ - Resources Reference: mcp/resources.md
137
+ - Integration Guide: mcp/integration.md
138
+ - API Reference:
139
+ - Ruby Library: api/ruby-library.md
140
+ - Task Model: api/task-model.md
141
+ - Database: api/database.md
142
+
143
+ copyright: Copyright &copy; 2025 Dewayne VanHoozer