ace-assign 0.37.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 +7 -0
- data/.ace-defaults/assign/catalog/composition-rules.yml +211 -0
- data/.ace-defaults/assign/catalog/recipes/batch-tasks.recipe.yml +44 -0
- data/.ace-defaults/assign/catalog/recipes/documentation.recipe.yml +35 -0
- data/.ace-defaults/assign/catalog/recipes/fix-and-review.recipe.yml +32 -0
- data/.ace-defaults/assign/catalog/recipes/implement-simple.recipe.yml +29 -0
- data/.ace-defaults/assign/catalog/recipes/implement-with-pr.recipe.yml +48 -0
- data/.ace-defaults/assign/catalog/recipes/release-only.recipe.yml +34 -0
- data/.ace-defaults/assign/catalog/steps/apply-feedback.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/commit.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/fix-tests.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/lint.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +57 -0
- data/.ace-defaults/assign/catalog/steps/onboard-base.step.yml +19 -0
- data/.ace-defaults/assign/catalog/steps/onboard.step.yml +19 -0
- data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +17 -0
- data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +34 -0
- data/.ace-defaults/assign/catalog/steps/push-to-remote.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/rebase-with-main.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +57 -0
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +23 -0
- data/.ace-defaults/assign/catalog/steps/release.step.yml +23 -0
- data/.ace-defaults/assign/catalog/steps/reorganize-commits.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/research.step.yml +19 -0
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/security-audit.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +25 -0
- data/.ace-defaults/assign/catalog/steps/squash-changelog.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/task-load.step.yml +29 -0
- data/.ace-defaults/assign/catalog/steps/update-docs.step.yml +38 -0
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +42 -0
- data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +48 -0
- data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +36 -0
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +23 -0
- data/.ace-defaults/assign/config.yml +48 -0
- data/.ace-defaults/assign/presets/fix-bug.yml +65 -0
- data/.ace-defaults/assign/presets/quick-implement.yml +41 -0
- data/.ace-defaults/assign/presets/release-only.yml +35 -0
- data/.ace-defaults/assign/presets/work-on-docs.yml +41 -0
- data/.ace-defaults/assign/presets/work-on-task.yml +179 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-assign.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-assign.yml +19 -0
- data/CHANGELOG.md +1415 -0
- data/README.md +87 -0
- data/Rakefile +16 -0
- data/docs/exit-codes.md +61 -0
- data/docs/getting-started.md +121 -0
- data/docs/handbook.md +40 -0
- data/docs/usage.md +224 -0
- data/exe/ace-assign +16 -0
- data/handbook/guides/fork-context.g.md +231 -0
- data/handbook/skills/as-assign-compose/SKILL.md +24 -0
- data/handbook/skills/as-assign-create/SKILL.md +23 -0
- data/handbook/skills/as-assign-drive/SKILL.md +24 -0
- data/handbook/skills/as-assign-prepare/SKILL.md +23 -0
- data/handbook/skills/as-assign-recover-fork/SKILL.md +22 -0
- data/handbook/skills/as-assign-run-in-batches/SKILL.md +23 -0
- data/handbook/skills/as-assign-start/SKILL.md +25 -0
- data/handbook/workflow-instructions/assign/compose.wf.md +256 -0
- data/handbook/workflow-instructions/assign/create.wf.md +215 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +666 -0
- data/handbook/workflow-instructions/assign/prepare.wf.md +469 -0
- data/handbook/workflow-instructions/assign/recover-fork.wf.md +233 -0
- data/handbook/workflow-instructions/assign/run-in-batches.wf.md +212 -0
- data/handbook/workflow-instructions/assign/start.wf.md +46 -0
- data/lib/ace/assign/atoms/assign_frontmatter_parser.rb +173 -0
- data/lib/ace/assign/atoms/catalog_loader.rb +101 -0
- data/lib/ace/assign/atoms/composition_rules.rb +219 -0
- data/lib/ace/assign/atoms/number_generator.rb +110 -0
- data/lib/ace/assign/atoms/preset_expander.rb +277 -0
- data/lib/ace/assign/atoms/step_file_parser.rb +207 -0
- data/lib/ace/assign/atoms/step_numbering.rb +227 -0
- data/lib/ace/assign/atoms/step_sorter.rb +66 -0
- data/lib/ace/assign/atoms/tree_formatter.rb +106 -0
- data/lib/ace/assign/cli/commands/add.rb +102 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +55 -0
- data/lib/ace/assign/cli/commands/create.rb +63 -0
- data/lib/ace/assign/cli/commands/fail.rb +43 -0
- data/lib/ace/assign/cli/commands/finish.rb +88 -0
- data/lib/ace/assign/cli/commands/fork_run.rb +229 -0
- data/lib/ace/assign/cli/commands/list.rb +166 -0
- data/lib/ace/assign/cli/commands/retry_cmd.rb +42 -0
- data/lib/ace/assign/cli/commands/select.rb +45 -0
- data/lib/ace/assign/cli/commands/start.rb +40 -0
- data/lib/ace/assign/cli/commands/status.rb +407 -0
- data/lib/ace/assign/cli.rb +144 -0
- data/lib/ace/assign/models/assignment.rb +107 -0
- data/lib/ace/assign/models/assignment_info.rb +66 -0
- data/lib/ace/assign/models/queue_state.rb +326 -0
- data/lib/ace/assign/models/step.rb +197 -0
- data/lib/ace/assign/molecules/assignment_discoverer.rb +57 -0
- data/lib/ace/assign/molecules/assignment_manager.rb +276 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +102 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +130 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +376 -0
- data/lib/ace/assign/molecules/step_renumberer.rb +227 -0
- data/lib/ace/assign/molecules/step_writer.rb +246 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +1299 -0
- data/lib/ace/assign/version.rb +7 -0
- data/lib/ace/assign.rb +141 -0
- metadata +289 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Run In Batches Workflow
|
|
4
|
+
purpose: workflow instruction for reusable repeated-item orchestration with deterministic assignment creation
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-03-18
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Run In Batches Workflow
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Create a reusable repeated-item assignment from:
|
|
15
|
+
|
|
16
|
+
1. One instruction template
|
|
17
|
+
2. One explicit `--items` list
|
|
18
|
+
3. Optional execution modifiers (`--sequential`, `--max-parallel`, `--run`)
|
|
19
|
+
|
|
20
|
+
This workflow keeps creation deterministic:
|
|
21
|
+
|
|
22
|
+
1. Render hidden spec under `.ace-local/assign/jobs/`
|
|
23
|
+
2. Call `ace-assign create <hidden-spec-path>`
|
|
24
|
+
3. Optionally hand off to `/as-assign-drive <assignment-id>`
|
|
25
|
+
|
|
26
|
+
## Supported Inputs
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
/as-assign-run-in-batches "Run E2E scenario {{item}}" --items TS-001,TS-002 --run
|
|
30
|
+
/as-assign-run-in-batches "Update docs for {{item}}" --items ace-assign,ace-task --sequential
|
|
31
|
+
/as-assign-run-in-batches "Review {{item}}" --items ace-git,ace-docs,ace-lint --max-parallel 3
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Runtime Boundary (Hard Rule)
|
|
35
|
+
|
|
36
|
+
`ace-assign create FILE` remains the deterministic runtime boundary.
|
|
37
|
+
|
|
38
|
+
- Parse and normalize workflow arguments in this workflow layer.
|
|
39
|
+
- Render a concrete hidden spec file.
|
|
40
|
+
- Pass the hidden spec file path to `ace-assign create`.
|
|
41
|
+
- Do not add natural-language parsing inside `ace-assign create`.
|
|
42
|
+
|
|
43
|
+
## Process
|
|
44
|
+
|
|
45
|
+
### 1. Parse Input
|
|
46
|
+
|
|
47
|
+
Required:
|
|
48
|
+
- One instruction template string (first positional argument)
|
|
49
|
+
- `--items <comma-separated-list>`
|
|
50
|
+
|
|
51
|
+
Optional:
|
|
52
|
+
- `--sequential` (run children one-by-one, still in fork context)
|
|
53
|
+
- `--max-parallel <N>` (parallel mode only, default: `3`; treated as rolling in-flight concurrency cap)
|
|
54
|
+
- `--run` (immediate handoff to `/as-assign-drive`)
|
|
55
|
+
|
|
56
|
+
If template or `--items` is missing, fail with an actionable message.
|
|
57
|
+
|
|
58
|
+
### 2. Normalize and Validate Item List
|
|
59
|
+
|
|
60
|
+
Normalize `--items` as:
|
|
61
|
+
|
|
62
|
+
1. Split by commas
|
|
63
|
+
2. Trim whitespace around each item
|
|
64
|
+
3. Drop empty entries
|
|
65
|
+
4. Preserve original order
|
|
66
|
+
|
|
67
|
+
Validation:
|
|
68
|
+
- If normalized list is empty → fail
|
|
69
|
+
- If duplicates exist after normalization → fail
|
|
70
|
+
- If `--max-parallel` is provided and is not an integer >= 1 → fail
|
|
71
|
+
|
|
72
|
+
Example failure messages:
|
|
73
|
+
- `--items is required (example: --items TS-001,TS-002)`
|
|
74
|
+
- `--items produced no valid values after normalization`
|
|
75
|
+
- `--items contains duplicates after normalization: TS-002`
|
|
76
|
+
|
|
77
|
+
### 3. Render Per-Item Instructions
|
|
78
|
+
|
|
79
|
+
For each normalized item:
|
|
80
|
+
|
|
81
|
+
- If template contains `{{item}}`, substitute with the item value.
|
|
82
|
+
- If template omits `{{item}}`, prepend a deterministic line:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Target item: <item>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then append the original template text unchanged.
|
|
89
|
+
|
|
90
|
+
### 4. Render Hidden Spec
|
|
91
|
+
|
|
92
|
+
Create hidden spec directory if missing:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
mkdir -p .ace-local/assign/jobs
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Write hidden spec:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
.ace-local/assign/jobs/<timestamp>-run-in-batches.yml
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Minimal structure:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
session:
|
|
108
|
+
name: run-in-batches-<timestamp>
|
|
109
|
+
description: Execute repeated-item assignment for explicit items.
|
|
110
|
+
|
|
111
|
+
steps:
|
|
112
|
+
- number: "010"
|
|
113
|
+
name: batch-items
|
|
114
|
+
batch_parent: true
|
|
115
|
+
parallel: true # false when --sequential is set
|
|
116
|
+
max_parallel: 3 # default 3 when parallel=true and flag omitted
|
|
117
|
+
fork_retry_limit: 1
|
|
118
|
+
instructions: |
|
|
119
|
+
Batch container for repeated-item execution.
|
|
120
|
+
Items: <item-1>, <item-2>, <item-3>
|
|
121
|
+
Scheduler: parallel=true, max_parallel=3.
|
|
122
|
+
|
|
123
|
+
- number: "010.01"
|
|
124
|
+
name: run-<item-1>
|
|
125
|
+
parent: "010"
|
|
126
|
+
context: fork
|
|
127
|
+
parallel: true # mirrors parent mode; false when --sequential
|
|
128
|
+
instructions: |
|
|
129
|
+
<rendered instruction for item-1>
|
|
130
|
+
|
|
131
|
+
- number: "010.02"
|
|
132
|
+
name: run-<item-2>
|
|
133
|
+
parent: "010"
|
|
134
|
+
context: fork
|
|
135
|
+
parallel: true # mirrors parent mode; false when --sequential
|
|
136
|
+
instructions: |
|
|
137
|
+
<rendered instruction for item-2>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Rules:
|
|
141
|
+
- Always create one parent plus one child per item.
|
|
142
|
+
- A single item still creates a valid parent/child tree.
|
|
143
|
+
- Child steps always keep `context: fork` so every item is delegated in a forked environment.
|
|
144
|
+
- `--sequential` sets `parallel: false` and `max_parallel: 1` on parent/children.
|
|
145
|
+
- Parallel mode sets `parallel: true`; `max_parallel` is explicit or defaults to `3`.
|
|
146
|
+
- In parallel mode, `max_parallel` means maximum concurrent in-flight children; it is not a fixed wave size.
|
|
147
|
+
- Parent and child metadata are workflow-level scheduling hints consumed by `/as-assign-drive`.
|
|
148
|
+
- Each invocation writes a new hidden spec file.
|
|
149
|
+
|
|
150
|
+
### 5. Create Assignment Deterministically
|
|
151
|
+
|
|
152
|
+
Invoke:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
ace-assign create .ace-local/assign/jobs/<timestamp>-run-in-batches.yml
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 6. Optional Immediate Handoff (`--run`)
|
|
159
|
+
|
|
160
|
+
If `--run` is present:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
/as-assign-drive <assignment-id>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
If no workable step is available, keep creation successful and report why drive did not continue.
|
|
167
|
+
|
|
168
|
+
### 7. Report Result
|
|
169
|
+
|
|
170
|
+
Show:
|
|
171
|
+
- Assignment ID and name
|
|
172
|
+
- Assignment path
|
|
173
|
+
- Hidden spec provenance path
|
|
174
|
+
- Whether drive handoff ran
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
| Scenario | Action |
|
|
179
|
+
|----------|--------|
|
|
180
|
+
| Missing template | Fail with usage + example |
|
|
181
|
+
| Missing `--items` | Fail with actionable message |
|
|
182
|
+
| Empty normalized items | Fail; no assignment created |
|
|
183
|
+
| Duplicate normalized items | Fail; no assignment created |
|
|
184
|
+
| Invalid `--max-parallel` | Fail; no assignment created |
|
|
185
|
+
| Hidden-spec render failure | Fail with concrete error |
|
|
186
|
+
| `ace-assign create` rejection | Surface CLI error unchanged |
|
|
187
|
+
| `--run` requested but no workable step | Keep create success; report handoff reason |
|
|
188
|
+
|
|
189
|
+
## Edge Cases
|
|
190
|
+
|
|
191
|
+
- Template omits `{{item}}` → prepend deterministic `Target item:` line.
|
|
192
|
+
- Single-item list still uses parent + one child.
|
|
193
|
+
- `--sequential` keeps fork context and only changes scheduler metadata.
|
|
194
|
+
- Item normalization preserves order of first occurrences.
|
|
195
|
+
|
|
196
|
+
## Success Criteria
|
|
197
|
+
|
|
198
|
+
- `/as-assign-run-in-batches` accepts one template plus explicit `--items`
|
|
199
|
+
- Supports `--max-parallel` with default `3` when omitted in parallel mode
|
|
200
|
+
- Hidden spec is written under `.ace-local/assign/jobs/`
|
|
201
|
+
- Assignment has one parent plus one child step per item
|
|
202
|
+
- Child steps always include `context: fork`
|
|
203
|
+
- Parent/child metadata reflects scheduler intent (`parallel`, `max_parallel`, `fork_retry_limit`)
|
|
204
|
+
- `{{item}}` substitution and `Target item:` fallback are deterministic
|
|
205
|
+
- Optional `--run` handoff delegates to `/as-assign-drive`
|
|
206
|
+
|
|
207
|
+
## Verification
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Ensure new workflow exists and is discoverable
|
|
211
|
+
ace-bundle wfi://assign/run-in-batches
|
|
212
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Start Assignment Workflow (Legacy Compatibility)
|
|
4
|
+
purpose: preserve compatibility for as-assign-start while routing to public assign/create + assign/drive flow
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-03-18
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Start Assignment Workflow (Legacy Compatibility)
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
`assign/start` is retained as an orchestration compatibility layer for typed canonical skill examples.
|
|
15
|
+
It delegates to create/drive flows that compose steps from assign-capable canonical skills.
|
|
16
|
+
|
|
17
|
+
Primary public UX remains:
|
|
18
|
+
- `/as-assign-create ...`
|
|
19
|
+
- `/as-assign-drive <assignment-id>`
|
|
20
|
+
|
|
21
|
+
## Process
|
|
22
|
+
|
|
23
|
+
1. Parse incoming arguments.
|
|
24
|
+
2. If `--run` is provided, run create with run handoff enabled:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
/as-assign-create $ARGUMENTS --run
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Otherwise, run create without immediate handoff:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
/as-assign-create $ARGUMENTS
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. If create succeeds and the caller requests explicit continuation, invoke:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
/as-assign-drive <assignment-id>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Success Criteria
|
|
43
|
+
|
|
44
|
+
- Preserves compatibility entrypoint for orchestration examples.
|
|
45
|
+
- Delegates behavior to `assign/create` and `assign/drive`.
|
|
46
|
+
- Does not redefine the public assignment flow.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Atoms
|
|
6
|
+
# Pure function: extracts and validates `assign:` block from markdown frontmatter.
|
|
7
|
+
#
|
|
8
|
+
# Takes a raw YAML frontmatter hash (already parsed from a .s.md or .wf.md file),
|
|
9
|
+
# extracts the `assign:` block, validates fields, and returns a structured result.
|
|
10
|
+
#
|
|
11
|
+
# No file I/O — reuses existing frontmatter extraction from StepFileParser or ace-support-markdown.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# frontmatter = { "id" => "v.0.9.0+task.148", "status" => "in-progress",
|
|
15
|
+
# "assign" => { "goal" => "implement-with-pr", "variables" => { "taskref" => "148" } } }
|
|
16
|
+
# result = AssignFrontmatterParser.parse(frontmatter)
|
|
17
|
+
# result[:config][:goal] # => "implement-with-pr"
|
|
18
|
+
# result[:valid] # => true
|
|
19
|
+
module AssignFrontmatterParser
|
|
20
|
+
VALID_FIELDS = %w[goal variables hints sub-steps context parent].freeze
|
|
21
|
+
VALID_HINT_ACTIONS = %w[include skip].freeze
|
|
22
|
+
VALID_CONTEXTS = %w[fork].freeze
|
|
23
|
+
|
|
24
|
+
# Parse and validate the assign: block from frontmatter.
|
|
25
|
+
#
|
|
26
|
+
# @param frontmatter [Hash] Full frontmatter hash from any .s.md or .wf.md file
|
|
27
|
+
# @return [Hash] { config: Hash|nil, valid: Boolean, errors: Array<String> }
|
|
28
|
+
def self.parse(frontmatter)
|
|
29
|
+
return {config: nil, valid: true, errors: []} if frontmatter.nil? || !frontmatter.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
assign_block = frontmatter["assign"]
|
|
32
|
+
return {config: nil, valid: true, errors: []} if assign_block.nil?
|
|
33
|
+
|
|
34
|
+
errors = validate(assign_block)
|
|
35
|
+
return {config: nil, valid: false, errors: errors} if errors.any?
|
|
36
|
+
|
|
37
|
+
config = extract_config(assign_block)
|
|
38
|
+
{config: config, valid: true, errors: []}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Validate the assign block fields.
|
|
42
|
+
#
|
|
43
|
+
# @param assign_block [Hash] The assign: block from frontmatter
|
|
44
|
+
# @return [Array<String>] List of validation errors (empty if valid)
|
|
45
|
+
def self.validate(assign_block)
|
|
46
|
+
errors = []
|
|
47
|
+
|
|
48
|
+
unless assign_block.is_a?(Hash)
|
|
49
|
+
errors << "assign: must be a mapping (Hash), got #{assign_block.class}"
|
|
50
|
+
return errors
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check for unknown fields
|
|
54
|
+
unknown = assign_block.keys - VALID_FIELDS
|
|
55
|
+
errors << "Unknown assign fields: #{unknown.join(", ")}" if unknown.any?
|
|
56
|
+
|
|
57
|
+
# Validate goal (string)
|
|
58
|
+
if assign_block.key?("goal") && !assign_block["goal"].is_a?(String)
|
|
59
|
+
errors << "assign.goal must be a string"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Validate variables (hash)
|
|
63
|
+
if assign_block.key?("variables") && !assign_block["variables"].is_a?(Hash)
|
|
64
|
+
errors << "assign.variables must be a mapping (Hash)"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validate hints (array of hashes with include/skip keys)
|
|
68
|
+
if assign_block.key?("hints")
|
|
69
|
+
errors.concat(validate_hints(assign_block["hints"]))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Validate sub-steps (array of strings)
|
|
73
|
+
if assign_block.key?("sub-steps")
|
|
74
|
+
if !assign_block["sub-steps"].is_a?(Array)
|
|
75
|
+
errors << "assign.sub-steps must be an array"
|
|
76
|
+
elsif assign_block["sub-steps"].any? { |s| !s.is_a?(String) }
|
|
77
|
+
errors << "assign.sub-steps entries must be strings"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Validate context (string, must be in VALID_CONTEXTS)
|
|
82
|
+
if assign_block.key?("context")
|
|
83
|
+
ctx = assign_block["context"]
|
|
84
|
+
if !ctx.is_a?(String)
|
|
85
|
+
errors << "assign.context must be a string"
|
|
86
|
+
elsif !VALID_CONTEXTS.include?(ctx)
|
|
87
|
+
errors << "assign.context must be one of: #{VALID_CONTEXTS.join(", ")}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Validate parent (string)
|
|
92
|
+
if assign_block.key?("parent") && !assign_block["parent"].is_a?(String)
|
|
93
|
+
errors << "assign.parent must be a string"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
errors
|
|
97
|
+
end
|
|
98
|
+
private_class_method :validate
|
|
99
|
+
|
|
100
|
+
# Extract structured config from a validated assign block.
|
|
101
|
+
#
|
|
102
|
+
# @param assign_block [Hash] Validated assign: block
|
|
103
|
+
# @return [Hash] Structured config with symbolized keys
|
|
104
|
+
def self.extract_config(assign_block)
|
|
105
|
+
{
|
|
106
|
+
goal: assign_block["goal"],
|
|
107
|
+
variables: assign_block["variables"] || {},
|
|
108
|
+
hints: normalize_hints(assign_block["hints"] || []),
|
|
109
|
+
sub_steps: assign_block["sub-steps"] || [],
|
|
110
|
+
context: assign_block["context"],
|
|
111
|
+
parent: assign_block["parent"]
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
private_class_method :extract_config
|
|
115
|
+
|
|
116
|
+
# Validate hints array entries.
|
|
117
|
+
#
|
|
118
|
+
# @param hints [Object] The hints value to validate
|
|
119
|
+
# @return [Array<String>] Validation errors
|
|
120
|
+
def self.validate_hints(hints)
|
|
121
|
+
errors = []
|
|
122
|
+
|
|
123
|
+
unless hints.is_a?(Array)
|
|
124
|
+
errors << "assign.hints must be an array"
|
|
125
|
+
return errors
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
hints.each_with_index do |hint, idx|
|
|
129
|
+
unless hint.is_a?(Hash)
|
|
130
|
+
errors << "assign.hints[#{idx}] must be a mapping (Hash)"
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
actions = hint.keys & VALID_HINT_ACTIONS
|
|
135
|
+
if actions.empty?
|
|
136
|
+
errors << "assign.hints[#{idx}] must have an 'include' or 'skip' key"
|
|
137
|
+
elsif actions.size > 1
|
|
138
|
+
errors << "assign.hints[#{idx}] cannot have both 'include' and 'skip'"
|
|
139
|
+
else
|
|
140
|
+
value = hint[actions.first]
|
|
141
|
+
unless value.is_a?(String)
|
|
142
|
+
errors << "assign.hints[#{idx}].#{actions.first} must be a string, got #{value.class}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
unknown_keys = hint.keys - VALID_HINT_ACTIONS
|
|
147
|
+
if unknown_keys.any?
|
|
148
|
+
errors << "assign.hints[#{idx}] has unknown keys: #{unknown_keys.join(", ")}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
errors
|
|
153
|
+
end
|
|
154
|
+
private_class_method :validate_hints
|
|
155
|
+
|
|
156
|
+
# Normalize hints into a consistent structure.
|
|
157
|
+
#
|
|
158
|
+
# @param hints [Array<Hash>] Raw hints from frontmatter
|
|
159
|
+
# @return [Array<Hash>] Normalized hints with :action and :step keys
|
|
160
|
+
def self.normalize_hints(hints)
|
|
161
|
+
hints.map do |hint|
|
|
162
|
+
if hint.key?("include")
|
|
163
|
+
{action: :include, step: hint["include"]}
|
|
164
|
+
elsif hint.key?("skip")
|
|
165
|
+
{action: :skip, step: hint["skip"]}
|
|
166
|
+
end
|
|
167
|
+
end.compact
|
|
168
|
+
end
|
|
169
|
+
private_class_method :normalize_hints
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Atoms
|
|
8
|
+
# Pure functions for loading and querying the step catalog.
|
|
9
|
+
#
|
|
10
|
+
# The step catalog is a directory of YAML files describing available
|
|
11
|
+
# step types, their prerequisites, artifacts, and metadata.
|
|
12
|
+
#
|
|
13
|
+
# @example Loading the catalog
|
|
14
|
+
# steps = CatalogLoader.load_all("/path/to/catalog/steps")
|
|
15
|
+
# # => [{ "name" => "onboard", "skill" => "onboard", ... }, ...]
|
|
16
|
+
#
|
|
17
|
+
# @example Finding a step
|
|
18
|
+
# step = CatalogLoader.find_by_name(steps, "work-on-task")
|
|
19
|
+
# # => { "name" => "work-on-task", "skill" => "ace:work-on-task", ... }
|
|
20
|
+
module CatalogLoader
|
|
21
|
+
# Load all step definitions from a catalog directory.
|
|
22
|
+
#
|
|
23
|
+
# @param steps_dir [String] Path to catalog/steps/ directory
|
|
24
|
+
# @return [Array<Hash>] Array of step definition hashes
|
|
25
|
+
def self.load_all(steps_dir)
|
|
26
|
+
return [] unless File.directory?(steps_dir)
|
|
27
|
+
|
|
28
|
+
Dir.glob(File.join(steps_dir, "*.step.yml")).sort.filter_map do |path|
|
|
29
|
+
parse_step_file(path)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Find a step definition by name.
|
|
34
|
+
#
|
|
35
|
+
# @param steps [Array<Hash>] Loaded step definitions
|
|
36
|
+
# @param name [String] Step name to find
|
|
37
|
+
# @return [Hash, nil] Step definition or nil if not found
|
|
38
|
+
def self.find_by_name(steps, name)
|
|
39
|
+
steps.find { |p| p["name"] == name }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Filter steps by tag.
|
|
43
|
+
#
|
|
44
|
+
# @param steps [Array<Hash>] Loaded step definitions
|
|
45
|
+
# @param tag [String] Tag to filter by
|
|
46
|
+
# @return [Array<Hash>] Steps matching the tag
|
|
47
|
+
def self.filter_by_tag(steps, tag)
|
|
48
|
+
steps.select { |p| (p["tags"] || []).include?(tag) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Find steps that produce a given artifact.
|
|
52
|
+
#
|
|
53
|
+
# @param steps [Array<Hash>] Loaded step definitions
|
|
54
|
+
# @param artifact [String] Artifact name (e.g., "code-changes")
|
|
55
|
+
# @return [Array<Hash>] Steps that produce the artifact
|
|
56
|
+
def self.producers_of(steps, artifact)
|
|
57
|
+
steps.select { |p| (p["produces"] || []).include?(artifact) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Validate that prerequisites are satisfied for a selection of steps.
|
|
61
|
+
#
|
|
62
|
+
# @param selected [Array<String>] Names of selected steps
|
|
63
|
+
# @param catalog [Array<Hash>] Full step catalog
|
|
64
|
+
# @return [Array<Hash>] Validation issues, each with :step, :prerequisite, :strength, :reason
|
|
65
|
+
def self.validate_prerequisites(selected, catalog)
|
|
66
|
+
issues = []
|
|
67
|
+
|
|
68
|
+
selected.each do |step_name|
|
|
69
|
+
step_def = find_by_name(catalog, step_name)
|
|
70
|
+
next unless step_def
|
|
71
|
+
|
|
72
|
+
(step_def["prerequisites"] || []).each do |prereq|
|
|
73
|
+
next if selected.include?(prereq["name"])
|
|
74
|
+
|
|
75
|
+
issues << {
|
|
76
|
+
step: step_name,
|
|
77
|
+
prerequisite: prereq["name"],
|
|
78
|
+
strength: prereq["strength"] || "recommended",
|
|
79
|
+
reason: prereq["reason"]
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
issues
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Parse a single step YAML file.
|
|
88
|
+
#
|
|
89
|
+
# @param path [String] File path
|
|
90
|
+
# @return [Hash, nil] Parsed step definition or nil on error
|
|
91
|
+
def self.parse_step_file(path)
|
|
92
|
+
YAML.safe_load_file(path, permitted_classes: [Date])
|
|
93
|
+
rescue => e
|
|
94
|
+
warn "Warning: Failed to parse step file #{path}: #{e.message}"
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
private_class_method :parse_step_file
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|