ace-task 0.39.3 → 0.41.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.
- checksums.yaml +4 -4
- data/.ace-defaults/nav/protocols/skill-sources/ace-task.yml +0 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-task.yml +0 -0
- data/.ace-defaults/task/config.yml +0 -0
- data/CHANGELOG.md +55 -0
- data/README.md +16 -3
- data/Rakefile +0 -0
- data/handbook/guides/task-definition.g.md +0 -0
- data/handbook/skills/as-bug-analyze/SKILL.md +0 -0
- data/handbook/skills/as-bug-fix/SKILL.md +0 -0
- data/handbook/skills/as-task-document-unplanned/SKILL.md +0 -0
- data/handbook/skills/as-task-draft/SKILL.md +0 -0
- data/handbook/skills/as-task-finder/SKILL.md +0 -0
- data/handbook/skills/as-task-plan/SKILL.md +0 -0
- data/handbook/skills/as-task-review/SKILL.md +0 -0
- data/handbook/skills/as-task-review-questions/SKILL.md +0 -0
- data/handbook/skills/as-task-update/SKILL.md +0 -0
- data/handbook/skills/as-task-work/SKILL.md +0 -0
- data/handbook/templates/task/draft.template.md +0 -0
- data/handbook/templates/task/file-modification-checklist.template.md +0 -0
- data/handbook/templates/task/technical-approach.template.md +0 -0
- data/handbook/workflow-instructions/bug/analyze.wf.md +0 -0
- data/handbook/workflow-instructions/bug/fix.wf.md +0 -0
- data/handbook/workflow-instructions/task/document-unplanned.wf.md +0 -0
- data/handbook/workflow-instructions/task/finder.wf.md +0 -0
- data/handbook/workflow-instructions/task/plan.wf.md +0 -0
- data/handbook/workflow-instructions/task/review-plan.wf.md +0 -0
- data/handbook/workflow-instructions/task/review-questions.wf.md +0 -0
- data/handbook/workflow-instructions/task/review-work.wf.md +0 -0
- data/handbook/workflow-instructions/task/review.wf.md +0 -0
- data/handbook/workflow-instructions/task/update.wf.md +0 -0
- data/lib/ace/task/atoms/task_file_pattern.rb +0 -0
- data/lib/ace/task/atoms/task_id_formatter.rb +0 -0
- data/lib/ace/task/atoms/task_validation_rules.rb +0 -0
- data/lib/ace/task/cli/commands/create.rb +85 -4
- data/lib/ace/task/cli/commands/doctor.rb +0 -0
- data/lib/ace/task/cli/commands/list.rb +4 -2
- data/lib/ace/task/cli/commands/plan.rb +2 -1
- data/lib/ace/task/cli/commands/show.rb +7 -1
- data/lib/ace/task/cli/commands/status.rb +0 -0
- data/lib/ace/task/cli/commands/update.rb +27 -2
- data/lib/ace/task/models/task.rb +0 -0
- data/lib/ace/task/molecules/git_ignored_path.rb +73 -0
- data/lib/ace/task/molecules/path_utils.rb +0 -0
- data/lib/ace/task/molecules/subtask_creator.rb +29 -16
- data/lib/ace/task/molecules/task_config_loader.rb +7 -1
- data/lib/ace/task/molecules/task_creator.rb +28 -9
- data/lib/ace/task/molecules/task_doctor_fixer.rb +16 -1
- data/lib/ace/task/molecules/task_doctor_reporter.rb +0 -0
- data/lib/ace/task/molecules/task_loader.rb +0 -0
- data/lib/ace/task/molecules/task_plan_cache.rb +7 -2
- data/lib/ace/task/molecules/task_plan_generator.rb +0 -0
- data/lib/ace/task/molecules/task_reparenter.rb +127 -30
- data/lib/ace/task/molecules/task_scanner.rb +2 -1
- data/lib/ace/task/molecules/task_structure_validator.rb +0 -0
- data/lib/ace/task/organisms/task_doctor.rb +7 -0
- data/lib/ace/task/organisms/task_manager.rb +153 -3
- data/lib/ace/task/version.rb +1 -1
- data/lib/ace/task.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a67866ad3602fb4570aca58dc63b0f60ea7575ce6baae62a6c1d6414970651c9
|
|
4
|
+
data.tar.gz: 7e547e93da96598b520866b605cf0368ec5beb2ff90c313c7118e649f029cae8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a86efa97d4a87a6e3de9561bb65a2cd9c0456ede8ece70e34c4a665776c4602c26977aa3557699c774118133571734c3266eba9b4523912225cf71f0f4aa4fd
|
|
7
|
+
data.tar.gz: e0301e6f51931b8b7320b01e362bab0d7988f67607660b0374e87b70b9bec524ed41e1640d7692bb914dc201553becb1a13057b036d949d6fef930ff0378929e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.41.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Detect Git-ignored task paths through Git itself (`git check-ignore` on the
|
|
15
|
+
actual path), from any working directory and with a custom task root, so
|
|
16
|
+
behavior for uncommitted task folders needs no folder-name knowledge.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Remove all folder-name exceptions from `ace-task`: no private/shared scope
|
|
21
|
+
constants, no special `_overseer` create destination, and no special default
|
|
22
|
+
listing. The default `ace-task list` is the ordinary root view; any existing
|
|
23
|
+
special folder stays reachable with `--in FOLDER` or `--in all`.
|
|
24
|
+
- Refuse `--git-commit` when the task path is Git-ignored — on create, subtask
|
|
25
|
+
creation, update, and move — instead of rejecting a named folder. The check
|
|
26
|
+
covers the task directory, individual children, the spec file, and the
|
|
27
|
+
destination paths a relocation would create (including archive partitions);
|
|
28
|
+
a rejected create rolls back the task artifacts and a rejected move/update
|
|
29
|
+
leaves the task unchanged.
|
|
30
|
+
- Do not auto-archive a completed Git-ignored task (or ignored family), and do
|
|
31
|
+
not report or perform an archive move for it in `ace-task doctor`; a move is
|
|
32
|
+
also skipped when the resulting archive destination is Git-ignored. The
|
|
33
|
+
folder owner decides when to delete obsolete history. No private archive
|
|
34
|
+
folder or retention mechanism is created.
|
|
35
|
+
- The canonical lab-flow contract records that the delegation tools
|
|
36
|
+
(`ace-git-worktree create --task`, `ace-assign create --task`) are role- and
|
|
37
|
+
folder-name agnostic: they do not reject management tasks and do not inspect
|
|
38
|
+
`.gitignore`; preparing a correct delegation stays with the caller. The
|
|
39
|
+
tools' own changelogs carry their release scope.
|
|
40
|
+
- Resolve the project task root from the project root (not the current
|
|
41
|
+
directory) for `list --in FOLDER` validation and for the plan cache/context
|
|
42
|
+
resolution, so `ace-task list --in _maybe` and `plan` work from a
|
|
43
|
+
subdirectory. `--root` remains an explicit path override resolved from the
|
|
44
|
+
current directory.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Judge the `--git-commit` preflight against the paths create actually writes:
|
|
49
|
+
the generated ID and title-derived slugs, or the allocated subtask character,
|
|
50
|
+
including the real placement folder. The previous sample-name probes could
|
|
51
|
+
reject a valid root task (for example with a `0-*/` ignore rule) even though
|
|
52
|
+
its generated path is tracked.
|
|
53
|
+
- Check the generated destination names of a reparent's whole family. Demoting
|
|
54
|
+
or promoting a task renames its own spec and every descendant spec, so a
|
|
55
|
+
guard using the pre-move names could pass while the new child spec is
|
|
56
|
+
Git-ignored — the commit then lost the child from the repository. The guard
|
|
57
|
+
now previews the actual renamed spec paths before mutating.
|
|
58
|
+
|
|
59
|
+
## [0.40.0] - 2026-10-16
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- Support exact --root selection in create, show and update so project objective queues can use the configured directory without changing the global _folder convention.
|
|
64
|
+
|
|
10
65
|
## [0.39.3] - 2026-10-12
|
|
11
66
|
|
|
12
67
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1> ACE - Task </h1>
|
|
3
3
|
|
|
4
|
-
Draft, organize, and tackle tasks
|
|
4
|
+
Draft, organize, and tackle tasks -- for you and your agents.
|
|
5
5
|
|
|
6
6
|
<img src="../docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
|
|
7
7
|
<br><br>
|
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
|
|
15
15
|
> Works with: Claude Code, Codex CLI, OpenCode, Gemini CLI, pi-agent, and more.
|
|
16
16
|
|
|
17
|
+
> **State owner:** `.ace-tasks/` -- project task markdown files and frontmatter
|
|
18
|
+
> owned by `ace-task`. Each project task gets one worktree under `.ace-wt/`.
|
|
19
|
+
> `ace-task` is folder-name agnostic: behavior that depends on Git tracking
|
|
20
|
+
> comes from Git's ignore rules for the actual path, never from a folder name.
|
|
21
|
+
> `--git-commit` is refused for Git-ignored tasks, and completed ignored tasks
|
|
22
|
+
> are neither auto-archived nor moved by `doctor --auto-fix`. A lab may prepare
|
|
23
|
+
> `.ace-tasks/_overseer/` (gitignored) and manage it with
|
|
24
|
+
> `ace-task --in _overseer`; the delegation tools are role- and folder-name
|
|
25
|
+
> agnostic and do not reject management tasks. The canonical flow is
|
|
26
|
+
> [docs/lab-flow.md](../docs/lab-flow.md).
|
|
27
|
+
|
|
17
28
|
[Getting Started](docs/getting-started.md) | [Usage Guide](docs/usage.md) | [Handbook - Skills, Agents, Templates](docs/handbook.md)
|
|
18
29
|
|
|
19
30
|

|
|
@@ -41,13 +52,15 @@ Tasks are markdown specs living in git - attached to branches and worktrees, not
|
|
|
41
52
|
|
|
42
53
|
**Plan the work** - generate a step-by-step implementation plan with `/as-task-plan` or `ace-task plan`. Define subtasks, configure which agent handles execution, and break scope into assignable units. The plan lives in the task spec so any agent can pick it up.
|
|
43
54
|
|
|
44
|
-
**
|
|
55
|
+
**Start task work in one command** - hand a task to [ace-overseer](../ace-overseer) and it provisions a worktree under `.ace-wt/`, prepares an assignment in `.ace-local/assign/`, and opens a terminal window (tmux or Herdr). One command kicks it off:
|
|
45
56
|
|
|
46
57
|
```bash
|
|
47
58
|
ace-overseer work-on --task t.tt6 --preset work-on-task
|
|
48
59
|
```
|
|
49
60
|
|
|
50
|
-
Under the hood this chains [ace-git-worktree](../ace-git-worktree) (isolated branch)
|
|
61
|
+
Under the hood this chains [ace-git-worktree](../ace-git-worktree) (isolated branch and worktree) → [ace-tmux](../ace-tmux) or [ace-herdr](../ace-herdr) (terminal) → [ace-assign](../ace-assign) (step-by-step plan). Switch to the window and run `/as-assign-drive` to walk through each step.
|
|
62
|
+
|
|
63
|
+
> **Planned (L6):** the private overseer queue that automatically continues already delegated, unblocked work is not available yet.
|
|
51
64
|
|
|
52
65
|
**Track progress** - `ace-task list` shows tasks by status - what is next, what is in progress, and what was recently completed. `ace-task status` gives a focused view of the current task with subtask progress.
|
|
53
66
|
|
data/Rakefile
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ace/support/cli"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require_relative "../../molecules/git_ignored_path"
|
|
5
6
|
|
|
6
7
|
module Ace
|
|
7
8
|
module Task
|
|
@@ -27,6 +28,7 @@ module Ace
|
|
|
27
28
|
'"Setup DB" --child-of q7w # Create as subtask',
|
|
28
29
|
'"Track issue" --issue 276 --server forgejo-lab # Link one exact issue',
|
|
29
30
|
'"Quick task" --in maybe # Create in _maybe/ folder',
|
|
31
|
+
'"Filed task" --in _review # Create in an existing custom folder',
|
|
30
32
|
'"Draft spec" --status draft --estimate TBD # Create as draft with estimate',
|
|
31
33
|
'"Preview only" --dry-run # Show what would be created'
|
|
32
34
|
]
|
|
@@ -43,11 +45,13 @@ module Ace
|
|
|
43
45
|
option :"default-server", type: :boolean, desc: "Use the configured default forge server"
|
|
44
46
|
option :"child-of", type: :string, desc: "Parent task reference (creates subtask)"
|
|
45
47
|
option :in, type: :string, aliases: %w[-i],
|
|
46
|
-
desc: "Top-level target (next, maybe, anytime, archive, or existing custom folder)"
|
|
48
|
+
desc: "Top-level target (next, maybe, anytime, archive, or an existing custom folder)"
|
|
47
49
|
option :"dry-run", type: :boolean, aliases: %w[-n], desc: "Preview without writing"
|
|
48
50
|
|
|
49
51
|
option :git_commit, type: :boolean, aliases: %w[--gc], desc: "Auto-commit changes"
|
|
50
52
|
|
|
53
|
+
option :root, type: :string, aliases: %w[-r], desc: "Exact task root directory (overrides project config)"
|
|
54
|
+
|
|
51
55
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
52
56
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
53
57
|
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
@@ -77,9 +81,24 @@ module Ace
|
|
|
77
81
|
end
|
|
78
82
|
end
|
|
79
83
|
|
|
80
|
-
manager =
|
|
84
|
+
manager = if options[:root]
|
|
85
|
+
Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(options[:root]))
|
|
86
|
+
else
|
|
87
|
+
Ace::Task::Organisms::TaskManager.new
|
|
88
|
+
end
|
|
81
89
|
validate_destination!(manager, in_folder) if in_folder && !child_of
|
|
82
|
-
|
|
90
|
+
# The preflight and the write resolve the same creation time, so
|
|
91
|
+
# the checked paths are exactly the generated paths create writes.
|
|
92
|
+
creation_time = Time.now.utc
|
|
93
|
+
if options[:git_commit]
|
|
94
|
+
reject_ignored_creation_target!(manager, title: title, in_folder: in_folder,
|
|
95
|
+
child_of: child_of, time: creation_time)
|
|
96
|
+
end
|
|
97
|
+
# Snapshot before any write so a rejected --git-commit can remove
|
|
98
|
+
# exactly the artifacts this command created (including a placed
|
|
99
|
+
# task, a new subtask, or an empty destination folder).
|
|
100
|
+
placement_directories_before = directories_under(manager.root_dir) if
|
|
101
|
+
(in_folder && !child_of) || options[:git_commit]
|
|
83
102
|
|
|
84
103
|
if dry_run
|
|
85
104
|
puts "Would create task:"
|
|
@@ -112,7 +131,8 @@ module Ace
|
|
|
112
131
|
priority: priority,
|
|
113
132
|
tags: tags,
|
|
114
133
|
estimate: estimate,
|
|
115
|
-
remote_issue: remote_issue
|
|
134
|
+
remote_issue: remote_issue,
|
|
135
|
+
time: creation_time
|
|
116
136
|
)
|
|
117
137
|
end
|
|
118
138
|
rescue Ace::Task::Organisms::TaskManager::CreateRetriesExhaustedError => e
|
|
@@ -129,6 +149,10 @@ module Ace
|
|
|
129
149
|
task = place_created_task(manager, task, in_folder, placement_directories_before)
|
|
130
150
|
end
|
|
131
151
|
|
|
152
|
+
if options[:git_commit]
|
|
153
|
+
reject_ignored_created_task!(manager, task, placement_directories_before)
|
|
154
|
+
end
|
|
155
|
+
|
|
132
156
|
puts "Created task #{task.id}"
|
|
133
157
|
puts " Path: #{task.file_path}"
|
|
134
158
|
if manager.last_update_note && !manager.last_update_note.strip.empty?
|
|
@@ -147,6 +171,63 @@ module Ace
|
|
|
147
171
|
|
|
148
172
|
private
|
|
149
173
|
|
|
174
|
+
# --git-commit preflight: resolve the known destination (the parent
|
|
175
|
+
# family or the placement folder) and the exact task paths this
|
|
176
|
+
# create call would write — generated ID and title-derived slugs, or
|
|
177
|
+
# the next subtask character — so ignore rules are judged against the
|
|
178
|
+
# real operation, never against sample names. The post-creation check
|
|
179
|
+
# stays authoritative for ID-collision retries and concurrent changes.
|
|
180
|
+
def reject_ignored_creation_target!(manager, title:, in_folder:, child_of:, time:)
|
|
181
|
+
detector = Ace::Support::Items::Atoms::SpecialFolderDetector
|
|
182
|
+
target = if child_of
|
|
183
|
+
manager.show(child_of)&.path
|
|
184
|
+
elsif in_folder && !detector.move_to_root?(in_folder)
|
|
185
|
+
File.join(manager.root_dir, detector.normalize(in_folder))
|
|
186
|
+
else
|
|
187
|
+
manager.root_dir
|
|
188
|
+
end
|
|
189
|
+
return unless target
|
|
190
|
+
|
|
191
|
+
if Ace::Task::Molecules::GitIgnoredPath.ignored?(target)
|
|
192
|
+
raise Ace::Support::Cli::Error,
|
|
193
|
+
"Cannot --git-commit into a Git-ignored destination: #{target}. " \
|
|
194
|
+
"Choose a tracked folder or omit --git-commit."
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
ignored = manager.planned_creation_paths(title, in_folder: in_folder, child_of: child_of, time: time)
|
|
198
|
+
.find { |path| Ace::Task::Molecules::GitIgnoredPath.ignored?(path) }
|
|
199
|
+
return unless ignored
|
|
200
|
+
|
|
201
|
+
raise Ace::Support::Cli::Error,
|
|
202
|
+
"Cannot --git-commit into a destination with Git-ignored child paths: #{ignored}. " \
|
|
203
|
+
"Choose a tracked folder or adjust the ignore rules."
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# --git-commit must not stage a task whose directory or spec path Git
|
|
207
|
+
# ignores. The check runs on the created paths (covering generated
|
|
208
|
+
# IDs, subtask folders, and the placed destination) and rolls the
|
|
209
|
+
# creation back, so a rejected commit leaves no task behind.
|
|
210
|
+
def reject_ignored_created_task!(manager, task, directories_before)
|
|
211
|
+
return unless Ace::Task::Molecules::GitIgnoredPath.ignored_task?(task)
|
|
212
|
+
|
|
213
|
+
begin
|
|
214
|
+
remove_created_task_artifacts(manager, task)
|
|
215
|
+
remove_empty_directories_created_since(manager.root_dir, directories_before)
|
|
216
|
+
rescue StandardError => cleanup_error
|
|
217
|
+
raise Ace::Support::Cli::Error,
|
|
218
|
+
"Cannot --git-commit a Git-ignored task path: #{task.path}. " \
|
|
219
|
+
"Cleanup also failed: #{cleanup_error.message}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
raise_ignored_commit!(task.path)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def raise_ignored_commit!(path)
|
|
226
|
+
raise Ace::Support::Cli::Error,
|
|
227
|
+
"Cannot --git-commit a Git-ignored task path: #{path}. " \
|
|
228
|
+
"Move the task to a tracked folder or omit --git-commit."
|
|
229
|
+
end
|
|
230
|
+
|
|
150
231
|
def validate_destination!(manager, target_folder)
|
|
151
232
|
detector = Ace::Support::Items::Atoms::SpecialFolderDetector
|
|
152
233
|
return if detector.move_to_root?(target_folder)
|
|
File without changes
|
|
@@ -20,9 +20,10 @@ module Ace
|
|
|
20
20
|
remove_const(:C)
|
|
21
21
|
|
|
22
22
|
example [
|
|
23
|
-
" #
|
|
23
|
+
" # Root tasks (default next view)",
|
|
24
24
|
"--in all # All tasks including archived/maybe",
|
|
25
25
|
"--in maybe # Tasks in _maybe/",
|
|
26
|
+
"--in archive # Archived tasks",
|
|
26
27
|
"--status pending # Filter by status",
|
|
27
28
|
"--tags ux,design # Tasks matching any tag",
|
|
28
29
|
"--in next --status pending # Combined filters",
|
|
@@ -33,7 +34,8 @@ module Ace
|
|
|
33
34
|
|
|
34
35
|
option :status, type: :string, aliases: %w[-s], desc: "Filter by status (pending, in-progress, done, blocked)"
|
|
35
36
|
option :tags, type: :string, aliases: %w[-T], desc: "Filter by tags (comma-separated, any match)"
|
|
36
|
-
option :in, type: :string, aliases: %w[-i],
|
|
37
|
+
option :in, type: :string, aliases: %w[-i],
|
|
38
|
+
desc: "Folder (next [default: root tasks], all, maybe, archive, or an existing special folder)"
|
|
37
39
|
option :root, type: :string, aliases: %w[-r], desc: "Override root path (subpath within tasks root)"
|
|
38
40
|
option :filter, type: :array, aliases: %w[-f], desc: "Filter by key:value (repeatable, supports key:a|b and key:!value)"
|
|
39
41
|
option :sort, type: :string, aliases: %w[-S], desc: "Sort order: smart (default, dependency-aware), id, priority, created"
|
|
@@ -89,7 +89,8 @@ module Ace
|
|
|
89
89
|
|
|
90
90
|
def capture_context_files(task)
|
|
91
91
|
files = Array(task.metadata.dig("bundle", "files"))
|
|
92
|
-
|
|
92
|
+
project_root = Molecules::TaskConfigLoader.project_root
|
|
93
|
+
expanded = files.map { |path| File.expand_path(path, project_root) }.uniq
|
|
93
94
|
resolved = expanded.select { |path| File.file?(path) }
|
|
94
95
|
|
|
95
96
|
missing = expanded - resolved
|
|
@@ -31,12 +31,18 @@ module Ace
|
|
|
31
31
|
option :content, type: :boolean, desc: "Print raw markdown content"
|
|
32
32
|
option :tree, type: :boolean, desc: "Show parent + subtask tree view"
|
|
33
33
|
|
|
34
|
+
option :root, type: :string, aliases: %w[-r], desc: "Exact task root directory (overrides project config)"
|
|
35
|
+
|
|
34
36
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
35
37
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
36
38
|
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
37
39
|
|
|
38
40
|
def call(ref:, **options)
|
|
39
|
-
manager =
|
|
41
|
+
manager = if options[:root]
|
|
42
|
+
Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(options[:root]))
|
|
43
|
+
else
|
|
44
|
+
Ace::Task::Organisms::TaskManager.new
|
|
45
|
+
end
|
|
40
46
|
task = manager.show(ref)
|
|
41
47
|
|
|
42
48
|
unless task
|
|
File without changes
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ace/support/cli"
|
|
4
4
|
require "ace/support/items"
|
|
5
|
+
require_relative "../../molecules/git_ignored_path"
|
|
5
6
|
|
|
6
7
|
module Ace
|
|
7
8
|
module Task
|
|
@@ -26,6 +27,7 @@ module Ace
|
|
|
26
27
|
"q7w --set worktree.branch=my-branch",
|
|
27
28
|
"q7w --set status=done --move-to archive",
|
|
28
29
|
"q7w --move-to next",
|
|
30
|
+
"q7w --move-to _maybe # Move to an existing special folder",
|
|
29
31
|
"q7w.a --move-as-child-of none # Promote subtask to standalone",
|
|
30
32
|
"q7w --move-as-child-of self # Convert to orchestrator",
|
|
31
33
|
"q7w --move-as-child-of abc # Demote to subtask of abc",
|
|
@@ -40,12 +42,15 @@ module Ace
|
|
|
40
42
|
option :set, type: :array, desc: "Set field: key=value (comma-separated for multiple)"
|
|
41
43
|
option :add, type: :array, desc: "Add to array field: key=value (comma-separated for multiple)"
|
|
42
44
|
option :remove, type: :array, desc: "Remove from array field: key=value (comma-separated for multiple)"
|
|
43
|
-
option :move_to, type: :string, aliases: %w[-m],
|
|
45
|
+
option :move_to, type: :string, aliases: %w[-m],
|
|
46
|
+
desc: "Move to folder (archive, maybe, anytime, next, or an existing custom folder)"
|
|
44
47
|
option :move_as_child_of, type: :string, desc: "Reparent: <parent_ref>, 'none' (promote), 'self' (orchestrator)"
|
|
45
48
|
option :position, type: :string, aliases: %w[-p], desc: "Set position: first, last, after:<ref>, before:<ref>"
|
|
46
49
|
|
|
47
50
|
option :git_commit, type: :boolean, aliases: %w[--gc], desc: "Auto-commit changes"
|
|
48
51
|
|
|
52
|
+
option :root, type: :string, aliases: %w[-r], desc: "Exact task root directory (overrides project config)"
|
|
53
|
+
|
|
49
54
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
50
55
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
51
56
|
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
@@ -75,7 +80,13 @@ module Ace
|
|
|
75
80
|
add_hash = parse_kv_pairs(add_args)
|
|
76
81
|
remove_hash = parse_kv_pairs(remove_args)
|
|
77
82
|
|
|
78
|
-
manager =
|
|
83
|
+
manager = if options[:root]
|
|
84
|
+
Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(options[:root]))
|
|
85
|
+
else
|
|
86
|
+
Ace::Task::Organisms::TaskManager.new
|
|
87
|
+
end
|
|
88
|
+
reject_ignored_git_commit!(manager, ref: ref, move_to: move_to,
|
|
89
|
+
move_as_child_of: move_as_child) if options[:git_commit]
|
|
79
90
|
|
|
80
91
|
# Resolve --position into a set or remove operation
|
|
81
92
|
if position_arg
|
|
@@ -120,6 +131,20 @@ module Ace
|
|
|
120
131
|
|
|
121
132
|
private
|
|
122
133
|
|
|
134
|
+
# --git-commit must not stage a task whose path Git ignores. Both the
|
|
135
|
+
# current paths and the paths the relocation would create are
|
|
136
|
+
# resolved before mutation: an ignore rule may target the destination
|
|
137
|
+
# folder, its children, individual spec files, or archive partitions.
|
|
138
|
+
def reject_ignored_git_commit!(manager, ref:, move_to:, move_as_child_of:)
|
|
139
|
+
paths = manager.commit_guard_paths(ref, move_to: move_to, move_as_child_of: move_as_child_of)
|
|
140
|
+
ignored = paths.find { |path| Ace::Task::Molecules::GitIgnoredPath.ignored?(path) }
|
|
141
|
+
return unless ignored
|
|
142
|
+
|
|
143
|
+
raise Ace::Support::Cli::Error,
|
|
144
|
+
"Cannot --git-commit a Git-ignored task path: #{ignored}. " \
|
|
145
|
+
"Move the task to a tracked folder or omit --git-commit."
|
|
146
|
+
end
|
|
147
|
+
|
|
123
148
|
# Resolve a --position argument into a B36TS value.
|
|
124
149
|
# Supports: first, last, after:<ref>, before:<ref>
|
|
125
150
|
def resolve_position(arg, manager)
|
data/lib/ace/task/models/task.rb
CHANGED
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/git"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Task
|
|
7
|
+
module Molecules
|
|
8
|
+
# Detects whether a task path is ignored by Git.
|
|
9
|
+
#
|
|
10
|
+
# ace-task is a universal task tool: it does not know which folders a lab
|
|
11
|
+
# or a role reserves. When behavior has to differ for a path that is not
|
|
12
|
+
# committed, the decision comes from Git's own ignore rules for the actual
|
|
13
|
+
# path — never from a folder-name list. The same query covers a task
|
|
14
|
+
# directory or its spec file, a path that does not exist yet (a destination
|
|
15
|
+
# computed before a move), any working directory, and a custom task root:
|
|
16
|
+
# Git resolves the repository from the path itself.
|
|
17
|
+
class GitIgnoredPath
|
|
18
|
+
# @param path [String] Path to check (file or directory, may not exist)
|
|
19
|
+
# @return [Boolean] true when Git ignores the path (a commit would skip it)
|
|
20
|
+
def self.ignored?(path)
|
|
21
|
+
absolute = File.expand_path(path.to_s)
|
|
22
|
+
working_dir = File.directory?(absolute) ? absolute : File.dirname(absolute)
|
|
23
|
+
working_dir = File.dirname(working_dir) until Dir.exist?(working_dir) ||
|
|
24
|
+
File.dirname(working_dir) == working_dir
|
|
25
|
+
return false unless Dir.exist?(working_dir)
|
|
26
|
+
|
|
27
|
+
result = Ace::Git::Atoms::CommandExecutor.execute(
|
|
28
|
+
"git", "-C", working_dir, "check-ignore", "-q", "--", absolute
|
|
29
|
+
)
|
|
30
|
+
result[:success] == true
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A task is uncommittable when its directory or its spec file is ignored:
|
|
36
|
+
# ignore rules may target the folder, individual children, or spec names.
|
|
37
|
+
#
|
|
38
|
+
# @param task [Models::Task]
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def self.ignored_task?(task)
|
|
41
|
+
ignored?(task.path) || ignored?(task.file_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A task family is uncommittable when its directory or any spec file
|
|
45
|
+
# inside it is ignored (auto-archive and doctor move whole families).
|
|
46
|
+
#
|
|
47
|
+
# @param dir_path [String]
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def self.ignored_family?(dir_path)
|
|
50
|
+
ignored?(dir_path) ||
|
|
51
|
+
Dir.glob(File.join(dir_path.to_s, "**", "*.s.md")).any? { |spec| ignored?(spec) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Relocating into an ignored destination would expose committed-looking
|
|
55
|
+
# content to normal staging, so both the destination directory and the
|
|
56
|
+
# destination paths of the moving specs must be checked before a move.
|
|
57
|
+
#
|
|
58
|
+
# @param source_dir [String] Current task/family directory
|
|
59
|
+
# @param destination_dir [String] Directory the move would create
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def self.ignored_after_relocation?(source_dir, destination_dir)
|
|
62
|
+
return true if ignored?(destination_dir)
|
|
63
|
+
|
|
64
|
+
source_root = File.expand_path(source_dir.to_s)
|
|
65
|
+
Dir.glob(File.join(source_root, "**", "*.s.md")).any? do |spec|
|
|
66
|
+
relative = spec.delete_prefix(source_root + File::SEPARATOR)
|
|
67
|
+
ignored?(File.join(destination_dir.to_s, relative))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
File without changes
|
|
@@ -44,20 +44,9 @@ module Ace
|
|
|
44
44
|
)
|
|
45
45
|
raise ArgumentError, "Title is required" if title.nil? || title.strip.empty?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Build subtask ID: parent_id + ".{char}"
|
|
52
|
-
subtask_id = "#{parent_task.id}.#{next_char}"
|
|
53
|
-
|
|
54
|
-
# Generate slugs (folder: 5 words, file: 7 words)
|
|
55
|
-
folder_slug = generate_folder_slug(title)
|
|
56
|
-
file_slug = generate_file_slug(title)
|
|
57
|
-
|
|
58
|
-
# Build folder and file names
|
|
59
|
-
folder_name = "#{next_char}-#{folder_slug}"
|
|
60
|
-
subtask_dir = File.join(parent_task.path, folder_name)
|
|
47
|
+
paths = destination_paths_for(parent_task, title)
|
|
48
|
+
subtask_id = paths[:id]
|
|
49
|
+
subtask_dir = paths[:dir]
|
|
61
50
|
FileUtils.mkdir_p(subtask_dir)
|
|
62
51
|
|
|
63
52
|
# Build frontmatter
|
|
@@ -74,8 +63,7 @@ module Ace
|
|
|
74
63
|
)
|
|
75
64
|
|
|
76
65
|
# Write spec file
|
|
77
|
-
|
|
78
|
-
spec_file = File.join(subtask_dir, spec_filename)
|
|
66
|
+
spec_file = paths[:spec]
|
|
79
67
|
content = build_spec_content(frontmatter: frontmatter, title: title)
|
|
80
68
|
File.write(spec_file, content)
|
|
81
69
|
|
|
@@ -84,8 +72,33 @@ module Ace
|
|
|
84
72
|
loader.load(subtask_dir, id: subtask_id, load_subtasks: false)
|
|
85
73
|
end
|
|
86
74
|
|
|
75
|
+
# Non-mutating preview of the directory and spec file a subtask create
|
|
76
|
+
# call would write: the next allocated subtask character and the
|
|
77
|
+
# title-derived slugs, matching create exactly. Pre-write checks use it
|
|
78
|
+
# to judge Git ignore rules against the real generated names.
|
|
79
|
+
#
|
|
80
|
+
# @param parent_task [Models::Task] Parent task
|
|
81
|
+
# @param title [String] Subtask title
|
|
82
|
+
# @return [Array<String>] Subtask directory and spec file path
|
|
83
|
+
def destination_paths(parent_task, title)
|
|
84
|
+
raise ArgumentError, "Title is required" if title.nil? || title.strip.empty?
|
|
85
|
+
|
|
86
|
+
paths = destination_paths_for(parent_task, title)
|
|
87
|
+
[paths[:dir], paths[:spec]]
|
|
88
|
+
end
|
|
89
|
+
|
|
87
90
|
private
|
|
88
91
|
|
|
92
|
+
def destination_paths_for(parent_task, title)
|
|
93
|
+
existing_chars = scan_existing_subtask_chars(parent_task.path, parent_task.id)
|
|
94
|
+
next_char = allocate_next_char(existing_chars)
|
|
95
|
+
subtask_id = "#{parent_task.id}.#{next_char}"
|
|
96
|
+
folder_slug = generate_folder_slug(title)
|
|
97
|
+
file_slug = generate_file_slug(title)
|
|
98
|
+
subtask_dir = File.join(parent_task.path, "#{next_char}-#{folder_slug}")
|
|
99
|
+
{id: subtask_id, dir: subtask_dir, spec: File.join(subtask_dir, "#{subtask_id}-#{file_slug}.s.md")}
|
|
100
|
+
end
|
|
101
|
+
|
|
89
102
|
# Scan parent directory for existing subtask folders and extract their chars.
|
|
90
103
|
def scan_existing_subtask_chars(parent_dir, _parent_id)
|
|
91
104
|
chars = []
|
|
@@ -31,6 +31,12 @@ module Ace
|
|
|
31
31
|
deep_merge(config, load_project_config)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Project root used to resolve relative configuration and file paths.
|
|
35
|
+
# @return [String] Absolute project root (current directory when no marker is found)
|
|
36
|
+
def self.project_root
|
|
37
|
+
Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
|
|
38
|
+
end
|
|
39
|
+
|
|
34
40
|
# Get the root directory for tasks
|
|
35
41
|
# @param config [Hash] Configuration hash
|
|
36
42
|
# @return [String] Absolute path to tasks root directory
|
|
@@ -41,7 +47,7 @@ module Ace
|
|
|
41
47
|
if dir.start_with?("/")
|
|
42
48
|
dir
|
|
43
49
|
else
|
|
44
|
-
File.join(
|
|
50
|
+
File.join(project_root, dir)
|
|
45
51
|
end
|
|
46
52
|
end
|
|
47
53
|
|