ace-task 0.41.0 → 0.41.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a67866ad3602fb4570aca58dc63b0f60ea7575ce6baae62a6c1d6414970651c9
4
- data.tar.gz: 7e547e93da96598b520866b605cf0368ec5beb2ff90c313c7118e649f029cae8
3
+ metadata.gz: 6e932999b7042c67c443a0221b3460edad42e09711a1f69e018f6a44473065f6
4
+ data.tar.gz: f993abd4008d257ce10e8dc105633294c31a1753fa76e5ea35e8355be3618ee2
5
5
  SHA512:
6
- metadata.gz: 9a86efa97d4a87a6e3de9561bb65a2cd9c0456ede8ece70e34c4a665776c4602c26977aa3557699c774118133571734c3266eba9b4523912225cf71f0f4aa4fd
7
- data.tar.gz: e0301e6f51931b8b7320b01e362bab0d7988f67607660b0374e87b70b9bec524ed41e1640d7692bb914dc201553becb1a13057b036d949d6fef930ff0378929e
6
+ metadata.gz: 6d508f8f9975cc717d0b4b788e824c5e800b5f5f5aae9e36639da1f9d98c9d6b95010371f8091ee18016d7548a23ffd6a7002b194cb4146e96e9dc2b1f8b5094
7
+ data.tar.gz: '08af4e6b32c752ce7253a147e98cdbfb8c5d2a930f8b51e06dcb5798403d85a592dadde99d854946c009cdf4e7774fa3b4e2eadce303d7ebc190d71e39a5c19b'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.41.1] - 2026-11-10
11
+
12
+ ### Fixed
13
+
14
+ - Serialize subtask allocation/reparenting across processes and path aliases. Resolve accessible task references without weakening full inventory errors; preserve private ignored-task ownership.
15
+
10
16
  ## [0.41.0] - 2026-10-17
11
17
 
12
18
  ### Added
data/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
  > `--git-commit` is refused for Git-ignored tasks, and completed ignored tasks
22
22
  > are neither auto-archived nor moved by `doctor --auto-fix`. A lab may prepare
23
23
  > `.ace-tasks/_overseer/` (gitignored) and manage it with
24
- > `ace-task --in _overseer`; the delegation tools are role- and folder-name
24
+ > `ace-task list --in _overseer`; the delegation tools are role- and folder-name
25
25
  > agnostic and do not reject management tasks. The canonical flow is
26
26
  > [docs/lab-flow.md](../docs/lab-flow.md).
27
27
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Task
5
+ module Molecules
6
+ # The parent directory inode is shared across processes and path aliases.
7
+ # Lock it across allocation and publication without adding task-store files.
8
+ module SubtaskAllocationLock
9
+ def self.synchronize(parent_path)
10
+ File.open(parent_path, File::RDONLY) do |directory|
11
+ directory.flock(File::LOCK_EX)
12
+ held = directory.stat
13
+ current = File.stat(parent_path)
14
+ unless current.directory? && [held.dev, held.ino] == [current.dev, current.ino]
15
+ raise ArgumentError, "Parent task moved while waiting for subtask allocation; retry"
16
+ end
17
+ yield
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require_relative "../atoms/task_id_formatter"
5
5
  require_relative "../atoms/task_frontmatter_defaults"
6
6
  require_relative "task_loader"
7
+ require_relative "subtask_allocation_lock"
7
8
 
8
9
  module Ace
9
10
  module Task
@@ -44,32 +45,34 @@ module Ace
44
45
  )
45
46
  raise ArgumentError, "Title is required" if title.nil? || title.strip.empty?
46
47
 
47
- paths = destination_paths_for(parent_task, title)
48
- subtask_id = paths[:id]
49
- subtask_dir = paths[:dir]
50
- FileUtils.mkdir_p(subtask_dir)
51
-
52
- # Build frontmatter
53
- effective_status = status || @config.dig("task", "default_status") || "pending"
54
- frontmatter = Atoms::TaskFrontmatterDefaults.build(
55
- id: subtask_id,
56
- status: effective_status,
57
- priority: priority,
58
- tags: tags,
59
- created_at: time,
60
- parent: parent_task.id,
61
- estimate: estimate,
62
- remote_issue: remote_issue
63
- )
64
-
65
- # Write spec file
66
- spec_file = paths[:spec]
67
- content = build_spec_content(frontmatter: frontmatter, title: title)
68
- File.write(spec_file, content)
69
-
70
- # Load and return
71
- loader = TaskLoader.new
72
- loader.load(subtask_dir, id: subtask_id, load_subtasks: false)
48
+ SubtaskAllocationLock.synchronize(parent_task.path) do
49
+ paths = destination_paths_for(parent_task, title)
50
+ subtask_id = paths[:id]
51
+ subtask_dir = paths[:dir]
52
+ FileUtils.mkdir_p(subtask_dir)
53
+
54
+ # Build frontmatter
55
+ effective_status = status || @config.dig("task", "default_status") || "pending"
56
+ frontmatter = Atoms::TaskFrontmatterDefaults.build(
57
+ id: subtask_id,
58
+ status: effective_status,
59
+ priority: priority,
60
+ tags: tags,
61
+ created_at: time,
62
+ parent: parent_task.id,
63
+ estimate: estimate,
64
+ remote_issue: remote_issue
65
+ )
66
+
67
+ # Write spec file
68
+ spec_file = paths[:spec]
69
+ content = build_spec_content(frontmatter: frontmatter, title: title)
70
+ File.write(spec_file, content)
71
+
72
+ # Load and return
73
+ loader = TaskLoader.new
74
+ loader.load(subtask_dir, id: subtask_id, load_subtasks: false)
75
+ end
73
76
  end
74
77
 
75
78
  # Non-mutating preview of the directory and spec file a subtask create
@@ -29,7 +29,7 @@ module Ace
29
29
  when "none"
30
30
  promote_to_standalone(task)
31
31
  when "self"
32
- convert_to_orchestrator(task)
32
+ SubtaskAllocationLock.synchronize(task.path) { convert_to_orchestrator(task) }
33
33
  else
34
34
  demote_to_subtask(task, parent_ref: target, resolve_ref: resolve_ref)
35
35
  end
@@ -110,6 +110,10 @@ module Ace
110
110
  raise ArgumentError, "Cannot convert subtask #{task.id} to orchestrator — promote first"
111
111
  end
112
112
 
113
+ unless scan_existing_subtask_chars(task.path, task.id).empty?
114
+ raise ArgumentError, "Task #{task.id} already has subtasks; cannot convert it again"
115
+ end
116
+
113
117
  paths = orchestrator_paths(task)
114
118
  subtask_id = paths[:id]
115
119
  subtask_dir = paths[:dir]
@@ -155,37 +159,39 @@ module Ace
155
159
  raise ArgumentError, "Cannot reparent task to itself"
156
160
  end
157
161
 
158
- paths = demoted_paths(task, parent_task)
159
- new_id = paths[:id]
160
- new_dir = paths[:dir]
161
- new_spec_basename = paths[:spec_basename]
162
+ SubtaskAllocationLock.synchronize(parent_task.path) do
163
+ paths = demoted_paths(task, parent_task)
164
+ new_id = paths[:id]
165
+ new_dir = paths[:dir]
166
+ new_spec_basename = paths[:spec_basename]
162
167
 
163
- raise ArgumentError, "Destination already exists: #{new_dir}" if File.exist?(new_dir)
168
+ raise ArgumentError, "Destination already exists: #{new_dir}" if File.exist?(new_dir)
164
169
 
165
- # Move folder into parent
166
- FileUtils.mv(task.path, new_dir)
170
+ # Move folder into parent
171
+ FileUtils.mv(task.path, new_dir)
167
172
 
168
- # Rename spec file
169
- old_spec_basename = File.basename(task.file_path)
170
- old_spec_in_new_dir = File.join(new_dir, old_spec_basename)
171
- new_spec_path = File.join(new_dir, new_spec_basename)
173
+ # Rename spec file
174
+ old_spec_basename = File.basename(task.file_path)
175
+ old_spec_in_new_dir = File.join(new_dir, old_spec_basename)
176
+ new_spec_path = File.join(new_dir, new_spec_basename)
172
177
 
173
- File.rename(old_spec_in_new_dir, new_spec_path) if old_spec_basename != new_spec_basename
178
+ File.rename(old_spec_in_new_dir, new_spec_path) if old_spec_basename != new_spec_basename
174
179
 
175
- # Update frontmatter: new ID, set parent
176
- update_frontmatter(new_spec_path) do |fm|
177
- fm["id"] = new_id
178
- fm["parent"] = parent_task.id
179
- end
180
+ # Update frontmatter: new ID, set parent
181
+ update_frontmatter(new_spec_path) do |fm|
182
+ fm["id"] = new_id
183
+ fm["parent"] = parent_task.id
184
+ end
180
185
 
181
- # Descendant identities hang off the parent ID: rewrite their
182
- # frontmatter and spec filenames so persisted IDs stay authoritative.
183
- id_map = {task.id => new_id}
184
- rewrite_descendants(new_dir, old_parent_id: task.id, new_parent_id: new_id, id_map: id_map)
185
- rewrite_dependency_references(id_map)
186
+ # Descendant identities hang off the parent ID: rewrite their
187
+ # frontmatter and spec filenames so persisted IDs stay authoritative.
188
+ id_map = {task.id => new_id}
189
+ rewrite_descendants(new_dir, old_parent_id: task.id, new_parent_id: new_id, id_map: id_map)
190
+ rewrite_dependency_references(id_map)
186
191
 
187
- loader = TaskLoader.new
188
- loader.load(new_dir, id: new_id)
192
+ loader = TaskLoader.new
193
+ loader.load(new_dir, id: new_id)
194
+ end
189
195
  end
190
196
 
191
197
  # Rewrite dependency references in every task spec after an ID map
@@ -35,8 +35,8 @@ module Ace
35
35
 
36
36
  # Scan for all primary tasks (excludes subtask folders).
37
37
  # @return [Array<ScanResult>] Sorted scan results
38
- def scan
39
- @scanner.scan.reject { |sr| subtask_folder?(sr.folder_name) }
38
+ def scan(accessible_only: false)
39
+ @scanner.scan(accessible_only: accessible_only).reject { |sr| subtask_folder?(sr.folder_name) }
40
40
  end
41
41
 
42
42
  # Scan and filter by special folder or virtual filter.
@@ -877,7 +877,7 @@ module Ace
877
877
 
878
878
  def resolve_scan_result(ref)
879
879
  scanner = Molecules::TaskScanner.new(@root_dir)
880
- scan_results = scanner.scan
880
+ scan_results = scanner.scan(accessible_only: true)
881
881
  resolver = Molecules::TaskResolver.new(scan_results)
882
882
  resolver.resolve(ref)
883
883
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Task
5
- VERSION = "0.41.0"
5
+ VERSION = "0.41.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-task
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.41.0
4
+ version: 0.41.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-20 00:00:00.000000000 Z
10
+ date: 2026-11-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-core
@@ -44,6 +44,9 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0.15'
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 0.15.17
47
50
  type: :runtime
48
51
  prerelease: false
49
52
  version_requirements: !ruby/object:Gem::Requirement
@@ -51,6 +54,9 @@ dependencies:
51
54
  - - "~>"
52
55
  - !ruby/object:Gem::Version
53
56
  version: '0.15'
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 0.15.17
54
60
  - !ruby/object:Gem::Dependency
55
61
  name: ace-support-markdown
56
62
  requirement: !ruby/object:Gem::Requirement
@@ -199,6 +205,7 @@ files:
199
205
  - lib/ace/task/molecules/issue_link.rb
200
206
  - lib/ace/task/molecules/issue_sync_adapter.rb
201
207
  - lib/ace/task/molecules/path_utils.rb
208
+ - lib/ace/task/molecules/subtask_allocation_lock.rb
202
209
  - lib/ace/task/molecules/subtask_creator.rb
203
210
  - lib/ace/task/molecules/task_config_loader.rb
204
211
  - lib/ace/task/molecules/task_creator.rb