ace-task 0.31.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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/protocols/skill-sources/ace-task.yml +19 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-task.yml +19 -0
  4. data/.ace-defaults/task/config.yml +25 -0
  5. data/CHANGELOG.md +518 -0
  6. data/README.md +52 -0
  7. data/Rakefile +12 -0
  8. data/exe/ace-task +22 -0
  9. data/handbook/guides/task-definition.g.md +156 -0
  10. data/handbook/skills/as-bug-analyze/SKILL.md +26 -0
  11. data/handbook/skills/as-bug-fix/SKILL.md +27 -0
  12. data/handbook/skills/as-task-document-unplanned/SKILL.md +27 -0
  13. data/handbook/skills/as-task-draft/SKILL.md +24 -0
  14. data/handbook/skills/as-task-finder/SKILL.md +27 -0
  15. data/handbook/skills/as-task-plan/SKILL.md +30 -0
  16. data/handbook/skills/as-task-review/SKILL.md +25 -0
  17. data/handbook/skills/as-task-review-questions/SKILL.md +25 -0
  18. data/handbook/skills/as-task-update/SKILL.md +21 -0
  19. data/handbook/skills/as-task-work/SKILL.md +41 -0
  20. data/handbook/templates/task/draft.template.md +166 -0
  21. data/handbook/templates/task/file-modification-checklist.template.md +26 -0
  22. data/handbook/templates/task/technical-approach.template.md +26 -0
  23. data/handbook/workflow-instructions/bug/analyze.wf.md +458 -0
  24. data/handbook/workflow-instructions/bug/fix.wf.md +512 -0
  25. data/handbook/workflow-instructions/task/document-unplanned.wf.md +222 -0
  26. data/handbook/workflow-instructions/task/draft.wf.md +552 -0
  27. data/handbook/workflow-instructions/task/finder.wf.md +22 -0
  28. data/handbook/workflow-instructions/task/plan.wf.md +489 -0
  29. data/handbook/workflow-instructions/task/review-plan.wf.md +144 -0
  30. data/handbook/workflow-instructions/task/review-questions.wf.md +411 -0
  31. data/handbook/workflow-instructions/task/review-work.wf.md +146 -0
  32. data/handbook/workflow-instructions/task/review.wf.md +351 -0
  33. data/handbook/workflow-instructions/task/update.wf.md +118 -0
  34. data/handbook/workflow-instructions/task/work.wf.md +106 -0
  35. data/lib/ace/task/atoms/task_file_pattern.rb +68 -0
  36. data/lib/ace/task/atoms/task_frontmatter_defaults.rb +46 -0
  37. data/lib/ace/task/atoms/task_id_formatter.rb +62 -0
  38. data/lib/ace/task/atoms/task_validation_rules.rb +51 -0
  39. data/lib/ace/task/cli/commands/create.rb +105 -0
  40. data/lib/ace/task/cli/commands/doctor.rb +206 -0
  41. data/lib/ace/task/cli/commands/list.rb +73 -0
  42. data/lib/ace/task/cli/commands/plan.rb +119 -0
  43. data/lib/ace/task/cli/commands/show.rb +58 -0
  44. data/lib/ace/task/cli/commands/status.rb +77 -0
  45. data/lib/ace/task/cli/commands/update.rb +183 -0
  46. data/lib/ace/task/cli.rb +83 -0
  47. data/lib/ace/task/models/task.rb +46 -0
  48. data/lib/ace/task/molecules/path_utils.rb +20 -0
  49. data/lib/ace/task/molecules/subtask_creator.rb +130 -0
  50. data/lib/ace/task/molecules/task_config_loader.rb +92 -0
  51. data/lib/ace/task/molecules/task_creator.rb +115 -0
  52. data/lib/ace/task/molecules/task_display_formatter.rb +221 -0
  53. data/lib/ace/task/molecules/task_doctor_fixer.rb +510 -0
  54. data/lib/ace/task/molecules/task_doctor_reporter.rb +264 -0
  55. data/lib/ace/task/molecules/task_frontmatter_validator.rb +138 -0
  56. data/lib/ace/task/molecules/task_loader.rb +119 -0
  57. data/lib/ace/task/molecules/task_plan_cache.rb +190 -0
  58. data/lib/ace/task/molecules/task_plan_generator.rb +141 -0
  59. data/lib/ace/task/molecules/task_plan_prompt_builder.rb +91 -0
  60. data/lib/ace/task/molecules/task_reparenter.rb +247 -0
  61. data/lib/ace/task/molecules/task_resolver.rb +115 -0
  62. data/lib/ace/task/molecules/task_scanner.rb +129 -0
  63. data/lib/ace/task/molecules/task_structure_validator.rb +154 -0
  64. data/lib/ace/task/organisms/task_doctor.rb +199 -0
  65. data/lib/ace/task/organisms/task_manager.rb +353 -0
  66. data/lib/ace/task/version.rb +7 -0
  67. data/lib/ace/task.rb +37 -0
  68. metadata +197 -0
@@ -0,0 +1,510 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+ require "ace/support/markdown"
6
+ require "ace/support/items"
7
+ require_relative "../atoms/task_id_formatter"
8
+ require_relative "../atoms/task_validation_rules"
9
+ require_relative "../atoms/task_frontmatter_defaults"
10
+ require_relative "task_scanner"
11
+
12
+ module Ace
13
+ module Task
14
+ module Molecules
15
+ # Handles auto-fixing of common task issues detected by doctor.
16
+ # Supports dry_run mode to preview fixes without applying them.
17
+ class TaskDoctorFixer
18
+ attr_reader :dry_run, :fixed_count, :skipped_count
19
+
20
+ def initialize(dry_run: false, root_dir: nil)
21
+ @dry_run = dry_run
22
+ @root_dir = root_dir
23
+ @fixed_count = 0
24
+ @skipped_count = 0
25
+ @fixes_applied = []
26
+ end
27
+
28
+ # Fix a batch of issues
29
+ # @param issues [Array<Hash>] Issues to fix
30
+ # @return [Hash] Fix results summary
31
+ def fix_issues(issues)
32
+ fixable_issues = issues.select { |issue| can_fix?(issue) }
33
+
34
+ fixable_issues.each do |issue|
35
+ fix_issue(issue)
36
+ end
37
+
38
+ {
39
+ fixed: @fixed_count,
40
+ skipped: @skipped_count,
41
+ fixes_applied: @fixes_applied,
42
+ dry_run: @dry_run
43
+ }
44
+ end
45
+
46
+ # Fix a single issue by pattern matching its message
47
+ # @param issue [Hash] Issue to fix
48
+ # @return [Boolean] Whether fix was successful
49
+ def fix_issue(issue)
50
+ case issue[:message]
51
+ when /Missing opening '---' delimiter/
52
+ fix_missing_opening_delimiter(issue[:location])
53
+ when /Missing closing '---' delimiter/
54
+ fix_missing_closing_delimiter(issue[:location])
55
+ when /Missing required field: id/
56
+ fix_missing_id(issue[:location])
57
+ when /Missing required field: status/,
58
+ /Missing recommended field: status/
59
+ fix_missing_status(issue[:location])
60
+ when /Missing required field: title/,
61
+ /Missing recommended field: title/
62
+ fix_missing_title(issue[:location])
63
+ when /Field 'tags' is not an array/
64
+ fix_tags_not_array(issue[:location])
65
+ when /Missing recommended field: tags/
66
+ fix_missing_tags(issue[:location])
67
+ when /Missing recommended field: created_at/
68
+ fix_missing_created_at(issue[:location])
69
+ when /terminal status.*not in _archive/
70
+ fix_move_to_archive(issue[:location])
71
+ when /in _archive\/ but status is/
72
+ fix_archive_status(issue[:location])
73
+ when /in _maybe\/ with terminal status/
74
+ fix_maybe_terminal(issue[:location])
75
+ when /Stale backup file/
76
+ fix_stale_backup(issue[:location])
77
+ when /Empty directory/
78
+ fix_empty_directory(issue[:location])
79
+ when /Folder name does not match '\{id\}-\{slug\}' convention/
80
+ fix_folder_naming(issue[:location])
81
+ else
82
+ @skipped_count += 1
83
+ false
84
+ end
85
+ end
86
+
87
+ # Check if an issue can be auto-fixed
88
+ # @param issue [Hash] Issue to check
89
+ # @return [Boolean]
90
+ def can_fix?(issue)
91
+ return false unless issue[:location]
92
+
93
+ FIXABLE_PATTERNS.any? { |pattern| issue[:message].match?(pattern) }
94
+ end
95
+
96
+ private
97
+
98
+ FIXABLE_PATTERNS = [
99
+ /Missing opening '---' delimiter/,
100
+ /Missing closing '---' delimiter/,
101
+ /Missing required field: id/,
102
+ /Missing required field: status/,
103
+ /Missing required field: title/,
104
+ /Missing recommended field: status/,
105
+ /Missing recommended field: title/,
106
+ /Missing recommended field: tags/,
107
+ /Missing recommended field: created_at/,
108
+ /Field 'tags' is not an array/,
109
+ /terminal status.*not in _archive/,
110
+ /in _archive\/ but status is/,
111
+ /in _maybe\/ with terminal status/,
112
+ /Stale backup file/,
113
+ /Empty directory/,
114
+ /Folder name does not match '\{id\}-\{slug\}' convention/
115
+ ].freeze
116
+
117
+ def fix_missing_closing_delimiter(file_path)
118
+ return false unless File.exist?(file_path)
119
+
120
+ content = File.read(file_path)
121
+ lines = content.lines
122
+ insert_idx = nil
123
+ lines[1..].each_with_index do |line, i|
124
+ if line.strip.empty? || line.start_with?("#")
125
+ insert_idx = i + 1
126
+ break
127
+ end
128
+ end
129
+ insert_idx ||= lines.size
130
+
131
+ fixed_lines = lines.dup
132
+ fixed_lines.insert(insert_idx, "---\n")
133
+ fixed_content = fixed_lines.join
134
+
135
+ apply_file_fix(file_path, fixed_content, "Added missing closing '---' delimiter")
136
+ end
137
+
138
+ def fix_missing_id(file_path)
139
+ return false unless File.exist?(file_path)
140
+
141
+ # Extract ID from folder name using task pattern (xxx.t.yyy)
142
+ dir_name = File.basename(File.dirname(file_path))
143
+ id_and_slug = TaskScanner::TASK_ID_EXTRACTOR.call(dir_name)
144
+ unless id_and_slug
145
+ return (@skipped_count += 1
146
+ false)
147
+ end
148
+
149
+ id = id_and_slug[0]
150
+ update_frontmatter_field(file_path, "id", id, "Added missing 'id' field from folder name")
151
+ end
152
+
153
+ def fix_missing_status(file_path)
154
+ update_frontmatter_field(file_path, "status", "pending", "Added missing 'status' field with default 'pending'")
155
+ end
156
+
157
+ def fix_missing_title(file_path)
158
+ return false unless File.exist?(file_path)
159
+
160
+ content = File.read(file_path)
161
+ # Try to extract title from body H1
162
+ title = nil
163
+ _fm, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
164
+ if body
165
+ h1_match = body.match(/^#\s+(.+)/)
166
+ title = h1_match[1].strip if h1_match
167
+ end
168
+
169
+ # Fallback: extract from folder slug
170
+ unless title
171
+ dir_name = File.basename(File.dirname(file_path))
172
+ id_and_slug = TaskScanner::TASK_ID_EXTRACTOR.call(dir_name)
173
+ title = if id_and_slug
174
+ id_and_slug[1].tr("-", " ").capitalize
175
+ else
176
+ "Untitled"
177
+ end
178
+ end
179
+
180
+ update_frontmatter_field(file_path, "title", title, "Added missing 'title' field: '#{title}'")
181
+ end
182
+
183
+ def fix_tags_not_array(file_path)
184
+ update_frontmatter_field(file_path, "tags", [], "Coerced 'tags' field to empty array")
185
+ end
186
+
187
+ def fix_missing_tags(file_path)
188
+ update_frontmatter_field(file_path, "tags", [], "Added missing 'tags' field with empty array")
189
+ end
190
+
191
+ def fix_missing_created_at(file_path)
192
+ return false unless File.exist?(file_path)
193
+
194
+ content = File.read(file_path)
195
+ frontmatter, _body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
196
+
197
+ id = frontmatter&.dig("id")
198
+ unless id
199
+ dir_name = File.basename(File.dirname(file_path))
200
+ id_and_slug = TaskScanner::TASK_ID_EXTRACTOR.call(dir_name)
201
+ id = id_and_slug[0] if id_and_slug
202
+ end
203
+
204
+ created_at = if id && Atoms::TaskValidationRules.valid_id?(id)
205
+ raw = Atoms::TaskIdFormatter.reconstruct(id)
206
+ Ace::B36ts.decode(raw).strftime("%Y-%m-%d %H:%M:%S")
207
+ else
208
+ Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")
209
+ end
210
+
211
+ update_frontmatter_field(file_path, "created_at", created_at, "Added missing 'created_at' field decoded from ID")
212
+ end
213
+
214
+ def fix_move_to_archive(file_path)
215
+ return false unless file_path && @root_dir
216
+
217
+ move_issue_to_archive(file_path, from_maybe: false)
218
+ end
219
+
220
+ def fix_archive_status(file_path)
221
+ update_frontmatter_field(file_path, "status", "done", "Updated status to 'done' (in _archive/)")
222
+ end
223
+
224
+ def fix_maybe_terminal(file_path)
225
+ return false unless file_path && @root_dir
226
+
227
+ move_issue_to_archive(file_path, from_maybe: true)
228
+ end
229
+
230
+ BACKUP_EXTENSIONS = /\.(backup\.\w+|tmp)$|~$/
231
+
232
+ def fix_stale_backup(file_path)
233
+ return false unless file_path && File.exist?(file_path)
234
+ unless File.basename(file_path).match?(BACKUP_EXTENSIONS)
235
+ @skipped_count += 1
236
+ return false
237
+ end
238
+
239
+ if @dry_run
240
+ log_fix(file_path, "Would delete stale backup file")
241
+ else
242
+ File.delete(file_path)
243
+ log_fix(file_path, "Deleted stale backup file")
244
+ end
245
+
246
+ @fixed_count += 1
247
+ true
248
+ end
249
+
250
+ def fix_empty_directory(dir_path)
251
+ return false unless dir_path && Dir.exist?(dir_path)
252
+
253
+ # Safety: only remove if truly empty
254
+ files = Dir.glob(File.join(dir_path, "**", "*")).select { |f| File.file?(f) }
255
+ unless files.empty?
256
+ @skipped_count += 1
257
+ return false
258
+ end
259
+
260
+ if @dry_run
261
+ log_fix(dir_path, "Would delete empty directory")
262
+ else
263
+ FileUtils.rm_rf(dir_path)
264
+ log_fix(dir_path, "Deleted empty directory")
265
+ end
266
+
267
+ @fixed_count += 1
268
+ true
269
+ end
270
+
271
+ def fix_missing_opening_delimiter(file_path)
272
+ return false unless File.exist?(file_path)
273
+
274
+ content = File.read(file_path)
275
+
276
+ # Extract ID from folder name
277
+ dir_name = File.basename(File.dirname(file_path))
278
+ id_and_slug = TaskScanner::TASK_ID_EXTRACTOR.call(dir_name)
279
+ id = id_and_slug ? id_and_slug[0] : nil
280
+
281
+ # Extract title from first H1 or folder slug
282
+ title = extract_title_from_content(content) || extract_slug_title(dir_name)
283
+
284
+ # Build minimal frontmatter
285
+ frontmatter = Atoms::TaskFrontmatterDefaults.build(
286
+ id: id || Atoms::TaskIdFormatter.generate.formatted_id,
287
+ status: "pending",
288
+ created_at: Time.now.utc
289
+ )
290
+ frontmatter["title"] = title
291
+ frontmatter["id"] = id if id # Use existing ID if available
292
+
293
+ # Prepend proper frontmatter structure
294
+ yaml_block = "---\n#{YAML.dump(frontmatter).sub(/^---\n/, "")}---\n"
295
+ new_content = "#{yaml_block}\n#{content}"
296
+
297
+ apply_file_fix(file_path, new_content, "Added opening '---' delimiter and frontmatter")
298
+ end
299
+
300
+ def fix_folder_naming(dir_path)
301
+ return false unless Dir.exist?(dir_path)
302
+
303
+ # Generate new valid ID
304
+ item_id = Atoms::TaskIdFormatter.generate
305
+ new_id = item_id.formatted_id
306
+
307
+ # Extract slug from old folder name
308
+ old_name = File.basename(dir_path)
309
+ slug = extract_slug_from_folder_name(old_name)
310
+
311
+ # Find spec file
312
+ spec_files = Dir.glob(File.join(dir_path, "*.s.md"))
313
+ .reject { |f| f.end_with?(".idea.s.md") }
314
+ if spec_files.empty?
315
+ return (@skipped_count += 1
316
+ false)
317
+ end
318
+
319
+ spec_file = spec_files.first
320
+
321
+ if @dry_run
322
+ new_folder_name = "#{new_id}-#{slug}"
323
+ log_fix(dir_path, "Would rename folder to #{new_folder_name}")
324
+ @fixed_count += 1
325
+ return true
326
+ end
327
+
328
+ # Update frontmatter id in spec file
329
+ editor = Ace::Support::Markdown::Organisms::DocumentEditor.new(spec_file)
330
+ editor.update_frontmatter("id" => new_id)
331
+ editor.save!(backup: true, validate_before: false)
332
+
333
+ # Build new names
334
+ new_folder_name = "#{new_id}-#{slug}"
335
+ parent = File.dirname(dir_path)
336
+ new_dir_path = File.join(parent, new_folder_name)
337
+
338
+ # Rename spec file
339
+ old_spec_name = File.basename(spec_file)
340
+ new_spec_name = "#{new_folder_name}.s.md"
341
+
342
+ # Rename folder
343
+ FileUtils.mv(dir_path, new_dir_path)
344
+ FileUtils.mv(File.join(new_dir_path, old_spec_name), File.join(new_dir_path, new_spec_name))
345
+
346
+ log_fix(dir_path, "Renamed folder to #{new_folder_name}")
347
+ @fixed_count += 1
348
+ true
349
+ rescue => e
350
+ log_fix(e.message, "Error: #{e.class}")
351
+ @skipped_count += 1
352
+ false
353
+ end
354
+
355
+ def extract_title_from_content(content)
356
+ h1_match = content.match(/^#\s+(.+)$/)
357
+ h1_match ? h1_match[1].strip : nil
358
+ end
359
+
360
+ def extract_slug_title(dir_name)
361
+ id_and_slug = TaskScanner::TASK_ID_EXTRACTOR.call(dir_name)
362
+ slug = id_and_slug ? id_and_slug[1] : dir_name
363
+ slug.tr("-", " ").capitalize
364
+ end
365
+
366
+ def extract_slug_from_folder_name(name)
367
+ # Remove task ID prefix patterns
368
+ slug = name.sub(/^[0-9a-z]{3}\.[a-z]\.[0-9a-z]{3}-/, "") # Remove xxx.t.yyy-
369
+ .sub(/^\d+-\d+-\d+-/, "") # Remove NNN-YYYYMMDD-HHMMSS-
370
+ .sub(/^\d{7,}-/, "") # Remove 7+ digit prefix
371
+ .sub(/^\d{6}-/, "") # Remove 6-digit date prefix
372
+ .sub(/^\d+-/, "") # Remove issue number prefix
373
+
374
+ if slug.empty? || slug.match?(/^\d+$/)
375
+ slug = name.gsub(/[^a-zA-Z0-9]+/, "-").downcase
376
+ end
377
+
378
+ slug = slug.gsub(/^-+|-+$/, "")
379
+ slug = "untitled" if slug.empty?
380
+ slug[0..50]
381
+ end
382
+
383
+ def update_frontmatter_field(file_path, field, value, description)
384
+ return false unless file_path && File.exist?(file_path)
385
+
386
+ if @dry_run
387
+ log_fix(file_path, "Would: #{description}")
388
+ @fixed_count += 1
389
+ return true
390
+ end
391
+
392
+ editor = Ace::Support::Markdown::Organisms::DocumentEditor.new(file_path)
393
+ editor.update_frontmatter(field => value)
394
+ editor.save!(backup: true, validate_before: false)
395
+ log_fix(file_path, description)
396
+ @fixed_count += 1
397
+ true
398
+ rescue => e
399
+ log_fix(e.message, "Error: #{e.class}")
400
+ @skipped_count += 1
401
+ false
402
+ end
403
+
404
+ def apply_file_fix(file_path, new_content, description)
405
+ if @dry_run
406
+ log_fix(file_path, "Would: #{description}")
407
+ else
408
+ Ace::Support::Markdown::Organisms::SafeFileWriter.write(
409
+ file_path,
410
+ new_content,
411
+ backup: true
412
+ )
413
+ log_fix(file_path, description)
414
+ end
415
+
416
+ @fixed_count += 1
417
+ true
418
+ rescue => e
419
+ log_fix(e.message, "Error: #{e.class}")
420
+ @skipped_count += 1
421
+ false
422
+ end
423
+
424
+ def log_fix(file_path, description)
425
+ @fixes_applied << {
426
+ file: file_path,
427
+ description: description,
428
+ timestamp: Time.now
429
+ }
430
+ end
431
+
432
+ def move_issue_to_archive(file_path, from_maybe:)
433
+ task_file = File.directory?(file_path) ? find_primary_spec(file_path) : file_path
434
+ unless task_file && File.exist?(task_file)
435
+ return (@skipped_count += 1
436
+ false)
437
+ end
438
+
439
+ task_dir = File.dirname(task_file)
440
+ content = File.read(task_file)
441
+ frontmatter, _body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
442
+ parent_id = frontmatter.is_a?(Hash) ? frontmatter["parent"] : nil
443
+ archive_time = parse_archive_time(frontmatter)
444
+
445
+ source_dir = task_dir
446
+ description = from_maybe ? "Moved from _maybe/ to _archive/" : "Moved to _archive/"
447
+
448
+ if parent_id
449
+ parent_dir = File.dirname(task_dir)
450
+ unless all_siblings_terminal?(parent_dir, parent_id)
451
+ log_fix(task_dir, "Skipped archive move for subtask (siblings are not all terminal)")
452
+ @skipped_count += 1
453
+ return false
454
+ end
455
+ source_dir = parent_dir
456
+ description = "Moved parent task to _archive/ (all subtasks terminal)"
457
+ end
458
+
459
+ if @dry_run
460
+ log_fix(source_dir, "Would #{description.downcase}")
461
+ @fixed_count += 1
462
+ return true
463
+ end
464
+
465
+ mover = Ace::Support::Items::Molecules::FolderMover.new(@root_dir)
466
+ mover.move(Struct.new(:path).new(source_dir), to: "archive", date: archive_time)
467
+ log_fix(source_dir, description)
468
+ @fixed_count += 1
469
+ true
470
+ rescue => e
471
+ log_fix(e.message, "Error: #{e.class}")
472
+ @skipped_count += 1
473
+ false
474
+ end
475
+
476
+ def find_primary_spec(dir_path)
477
+ Dir.glob(File.join(dir_path, "*.s.md"))
478
+ .reject { |path| path.end_with?(".idea.s.md") }
479
+ .first
480
+ end
481
+
482
+ def parse_archive_time(frontmatter)
483
+ return nil unless frontmatter.is_a?(Hash)
484
+
485
+ raw = frontmatter["completed_at"] || frontmatter["created_at"]
486
+ return nil unless raw
487
+ return raw if raw.is_a?(Time)
488
+ return raw.to_time if raw.respond_to?(:to_time)
489
+
490
+ Time.parse(raw.to_s)
491
+ rescue ArgumentError
492
+ nil
493
+ end
494
+
495
+ def all_siblings_terminal?(parent_dir, parent_id)
496
+ scanner = TaskScanner.new(@root_dir)
497
+ subtasks = scanner.scan_subtasks(parent_dir, parent_id: parent_id)
498
+ return false if subtasks.empty?
499
+
500
+ subtasks.all? do |scan_result|
501
+ content = File.read(scan_result.file_path)
502
+ frontmatter, _body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
503
+ status = frontmatter.is_a?(Hash) ? frontmatter["status"] : nil
504
+ Atoms::TaskValidationRules.terminal_status?(status.to_s.downcase)
505
+ end
506
+ end
507
+ end
508
+ end
509
+ end
510
+ end