ace-handbook 0.32.0 → 0.34.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e70a71fff6e61cbaa6724591eb965481b3b92153aed554c5fd6ad8b51f3e01e
4
- data.tar.gz: 99db5b08f74300bfbc87b71f2621a0bcee70934d92a89d7a142884adb8a2e434
3
+ metadata.gz: 6bac5cea7762cdf27f71764a6d79f8149763f83d6fc001157db3b869f0313869
4
+ data.tar.gz: 3062b5aa4e48907c9eeb26cc81d713ad8ccecdc948a9270e6324ea7e1eccd596
5
5
  SHA512:
6
- metadata.gz: 34c7119d2d01e4ad2ff58a6b91bc5b4b092fb42a575abc33319d3ee6f4c7e237058f1135c19ab560565ff3fa91a696e39a9b92e27b6b839f855de0bf20de0c64
7
- data.tar.gz: c1b28858004960a5ef381e50bde467910b6694bfb29179e7cdd7014d45d25a2eb47ece9d75996cef92e35c30892705ca7687a6340f169b86e07781a287f8e499
6
+ metadata.gz: 0fa6aa552edbfae44a919b680f396adf7dff0b1dfd4c7e26d5c9c9eae183719ec169604bebe58591e9854a1eafa41a700e676d9c1378bed14e7718283a676fb9
7
+ data.tar.gz: 6e61af1f361bcb6909fccbda5281a0342793ba0a79fd02369befdc29aa21ef5d30b2477725165e31bcbdf018c131ae17ab8a6cab740672c1f786e7a5d3d6ca66
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.34.0] - 2026-10-05
11
+
12
+ ### Changed
13
+
14
+ - Canonical delivery consumes neutral attempt-bound PR operations, current executed test/independent review receipts and exact authorized merge evidence; optional release preparation remains separate from publication.
15
+
16
+ ## [0.33.0] - 2026-09-28
17
+
18
+ ### Added
19
+ - Provider manifests may declare an optional `extensions_dir`; `ace-handbook sync` now projects extension assets from `ace-handbook-integration-*` packages (`handbook/extensions/**`) into that directory (idempotent copies; prunes only stale paths recorded in the `.ace-handbook-projection.json` receipt, leaving user-authored extensions untouched). Destinations are validated before anything is written; a failed projection rolls back overwritten **and** pruned assets so the previous installation stays complete and loadable, and the retry needs no manual cleanup. Sync summary output gains a `; extensions <dir> (N files, U updated, R removed)` segment when a provider projects extensions. `ProviderRegistry` exposes `extensions_dir(provider)` and `package_root(provider)`.
20
+
10
21
  ## [0.32.0] - 2026-09-27
11
22
 
12
23
  ### Added
@@ -1,272 +1,158 @@
1
1
  ---
2
2
  doc-type: workflow
3
- title: Perform Delivery Workflow
4
- purpose: Coordinate complete task delivery across implementation, review, release, and review cycles.
5
- ace-docs:
6
- last-updated: 2026-03-12
7
- last-checked: 2026-03-21
3
+ title: Perform attempt-bound delivery
4
+ purpose: Coordinate implementation, exact-head review and authorized delivery on the selected forge
8
5
  ---
9
6
 
10
- # Perform Delivery Workflow
7
+ # Perform attempt-bound delivery
11
8
 
12
9
  ## Goal
13
10
 
14
- Execute complete delivery workflow for a task with automatic step tracking, ensuring no steps are lost during complex multi-step execution.
11
+ Deliver one committed candidate in explicit standalone or managed mode, using
12
+ the selected forge and current executed tests and independent review.
15
13
 
16
14
  ## Prerequisites
17
15
 
18
- * Task reference (e.g., `215.03`) OR detailed inline instructions
19
- * Clean git working directory (or staged changes ready for commit)
20
- * Understanding of the standard delivery workflow steps
21
- * Access to the relevant ACE CLI tools and workflows
22
-
23
- ## Project Context Loading
24
-
25
- * Load task context via `ace-task show <ref>` if task reference provided
26
- * Load git status: `git status`
27
- * Load PR status: `gh pr view` (if PR exists)
28
-
29
- ## High-Level Execution Plan
30
-
31
- ### Phase 0: Externalize Workflow (CRITICAL - DO FIRST)
32
-
33
- * [ ] Parse input to identify delivery scope
34
- * [ ] Create TodoWrite entry for EVERY delivery step BEFORE starting work
35
- * [ ] Confirm todo list is complete before proceeding
36
-
37
- ### Phase 1: Implementation
38
-
39
- * [ ] Enter plan mode if implementation required
40
- * [ ] Execute implementation via `ace-bundle wfi://task/work` or inline instructions
41
- * [ ] Commit all changes (`ace-git-commit`)
42
- * [ ] Release modified packages (`ace-bundle wfi://release/local` if applicable)
43
- * [ ] Mark task done and push to remote
44
-
45
- ### Phase 2: PR & Initial Review
46
-
47
- * [ ] Create or update PR (`ace-bundle wfi://github/pr/create`)
48
- * [ ] Run initial review (`ace-bundle wfi://review/pr`)
49
- * [ ] Implement HIGH/CRITICAL feedback immediately
50
-
51
- ### Phase 3: Deep Review Cycle
52
-
53
- * [ ] Run deep code review (`ace-bundle wfi://review/pr`, using the `code-deep` preset)
54
- * [ ] Implement MEDIUM+ severity feedback
55
- * [ ] Run test suite (`ace-test-suite`)
56
- * [ ] Commit fixes (`ace-git-commit`)
57
- * [ ] Repeat review cycle if needed
58
-
59
- ## Process Steps
60
-
61
- ### Phase 0: Externalize Workflow (CRITICAL - DO FIRST)
62
-
63
- **BEFORE reading any detailed content or starting implementation:**
64
-
65
- 1. **Parse Input:**
66
- * Identify if input is a task reference (e.g., `215.03`) or inline instructions
67
- * Determine scope: full delivery vs. partial (implementation only, review only, etc.)
68
-
69
- 2. **Create Session Todo List IMMEDIATELY:**
70
- * Use TodoWrite to create entries for ALL delivery steps
71
- * This MUST be the FIRST action before any other work
72
- * Do not read implementation details until todo list is created
73
-
74
- **Standard Delivery Steps to Add:**
75
-
76
- ```
77
- 1. Implement task (via `ace-bundle wfi://task/work` or inline)
78
- 2. Commit all changes (`ace-git-commit`)
79
- 3. Release modified packages (`ace-bundle wfi://release/local`)
80
- 4. Mark task done and push to remote
81
- 5. Create/update PR (`ace-bundle wfi://github/pr/create`)
82
- 6. Initial review (`ace-bundle wfi://review/pr`)
83
- 7. Implement HIGH/CRITICAL feedback
84
- 8. Deep review (`ace-bundle wfi://review/pr` with `code-deep`)
85
- 9. Implement MEDIUM+ feedback
86
- 10. Run test suite and commit fixes
87
- ```
88
-
89
- 3. **Confirm Before Proceeding:**
90
- * Output: "Session todo list created with N delivery steps"
91
- * Only proceed to Phase 1 after todo list is confirmed
92
-
93
- ### Phase 1: Implementation
94
-
95
- 1. **Load Task Context (if task reference provided):**
96
- * Load `ace-bundle wfi://task/work`, then follow it with the task reference
97
- * If inline instructions provided, use those directly
98
-
99
- 2. **Enter Plan Mode (if implementation required):**
100
- * Prepare detailed implementation plan
101
- * Get user approval before proceeding
102
-
103
- 3. **Execute Implementation:**
104
- * Follow the implementation plan
105
- * Track progress using TodoWrite for sub-tasks
106
-
107
- 4. **Commit Changes:**
108
- * Run `ace-git-commit` to commit all changes
109
- * Use descriptive commit message
110
-
111
- 5. **Release Packages (if applicable):**
112
- * Load `ace-bundle wfi://release/local` when a release is required
113
- * Follow versioning conventions
114
-
115
- 6. **Complete Task:**
116
- * Mark task as done: `ace-task done <ref>`
117
- * Push to remote: `git push`
118
-
119
- ### Phase 2: PR & Initial Review
120
-
121
- 1. **Create or Update PR:**
122
- * Load `ace-bundle wfi://github/pr/create` to prepare the pull request flow
123
- * Include task reference and summary
124
-
125
- 2. **Run Initial Review:**
126
- * Load `ace-bundle wfi://review/pr`
127
- * Review the feedback output
128
-
129
- 3. **Implement Critical Feedback:**
130
- * Address all HIGH and CRITICAL severity items
131
- * Commit fixes with descriptive messages
132
-
133
- ### Phase 3: Deep Review Cycle
134
-
135
- 1. **Run Deep Code Review:**
136
- * Load `ace-bundle wfi://review/pr` and use the `code-deep` preset
137
- * This provides more thorough analysis
138
-
139
- 2. **Implement Feedback:**
140
- * Address MEDIUM severity and above items
141
- * Consider LOW severity items for future improvements
142
-
143
- 3. **Run Test Suite:**
144
- * Run `ace-test-suite` to verify all tests pass
145
- * Fix any failing tests
146
-
147
- 4. **Commit Fixes:**
148
- * Run `ace-git-commit` to commit all fixes
149
-
150
- 5. **Repeat Review Cycle (if needed):**
151
- * If significant changes were made, run another review
152
- * Continue until no HIGH/CRITICAL items remain
153
-
154
- ## Checkpoint Protocol
155
-
156
- **After completing EACH numbered todo item:**
157
-
158
- 1. Mark the todo as COMPLETED using TodoWrite
159
- 2. Output checkpoint message:
160
-
161
- ```
162
- ✓ Step N complete: [step description]
163
- → Next: Step N+1 - [next step description]
164
- ```
165
-
166
- 3. Read todo list to confirm next step
167
- 4. Continue IMMEDIATELY to next step (no user prompt needed unless blocked)
168
-
169
- **On Failure:**
170
-
171
- 1. Mark the current todo as blocked (keep as in_progress)
172
- 2. Output failure message:
173
-
174
- ```
175
- ✗ Step N failed: [step description]
176
- → Error: [error description]
177
- → Action needed: [what user needs to do]
178
- ```
179
-
180
- 3. Ask user for guidance using AskUserQuestion
181
- 4. Resume workflow after issue is resolved
182
-
183
- ## Auto-Continue Protocol
184
-
185
- **CRITICAL:** This workflow should AUTO-CONTINUE between steps without waiting for user input.
186
-
187
- The only times to pause and ask the user:
188
-
189
- * Plan mode approval (Phase 1)
190
- * Step failure requiring user intervention
191
- * Ambiguous instructions requiring clarification
192
- * External blockers (CI failures, merge conflicts, etc.)
193
-
194
- For all other transitions, immediately proceed to the next step after checkpoint.
195
-
196
- ## Scope Variations
197
-
198
- ### Full Delivery (default)
199
-
200
- All steps from implementation through review cycles. Load `ace-bundle wfi://handbook/perform-delivery`.
201
-
202
- ### Implementation Only
203
-
204
- Skip PR and review phases (useful for WIP branches). Load `ace-bundle wfi://handbook/perform-delivery`.
205
-
206
- ### Review Only
207
-
208
- Skip implementation, start from PR creation. Load `ace-bundle wfi://handbook/perform-delivery`.
209
-
210
- ### Custom Instructions
211
-
212
- Provide inline instructions instead of task reference. Load `ace-bundle wfi://handbook/perform-delivery`.
213
-
214
- ## Success Criteria
215
-
216
- * All todo items marked as completed
217
- * No skipped or forgotten steps
218
- * All commits pushed to remote
219
- * PR created (if applicable)
220
- * All HIGH/CRITICAL review feedback addressed
221
- * Test suite passing
222
- * Task marked as done in taskflow
223
-
224
- ## Error Handling
225
-
226
- **Step Fails to Complete:**
227
-
228
- * Keep todo item as in_progress
229
- * Log the error with context
230
- * Ask user for guidance
231
- * Resume from failed step after resolution
232
-
233
- **User Interruption:**
234
-
235
- * Save current state (which step is in progress)
236
- * Allow resume from interruption point
237
- * Maintain todo list across session
238
-
239
- **External Tool Failure:**
240
-
241
- * Report which tool failed and the error
242
- * Suggest manual alternatives if available
243
- * Wait for user guidance before proceeding
244
-
245
- ## Usage Examples
246
-
247
- ### Full Task Delivery
248
-
249
- > "Perform delivery for task 215.03"
250
-
251
- This will:
252
- 1. Load task context
253
- 2. Implement the task
254
- 3. Commit, release, mark done
255
- 4. Create PR
256
- 5. Review and iterate
257
-
258
- ### Review Cycle Only
259
-
260
- > "Perform delivery for task 215.03 --scope review"
261
-
262
- This will:
263
- 1. Create/update PR
264
- 2. Review and implement feedback
265
- 3. Run test suite
266
- 4. Iterate until clean
267
-
268
- ### Custom Workflow
269
-
270
- > "Perform delivery: implement the dark mode feature, commit, create PR, and run one review cycle"
271
-
272
- This will parse the custom instructions and create a todo list matching the requested steps.
16
+ - A reviewed task or an explicit implementation scope and project.
17
+ - For managed delivery, a managed assignment and an active scoped attempt for each
18
+ delivery action; use the actual assignment and
19
+ attempt IDs from the execution boundary, never caller-invented actor identity.
20
+ - For remote delivery, a pushed candidate and explicit fork/canonical provenance:
21
+ mode, head_repository_url, head_ref, base_repository_url and base_ref.
22
+ - An applicable authorization for each effect. Transport credentials do not grant
23
+ permission to merge, publish, deploy or synchronize.
24
+
25
+ Choose standalone mode for ordinary local task/inline implementation, tests, review
26
+ and optional requested version/changelog preparation without a managed assignment.
27
+ Choose managed mode when the request supplies an assignment or requires its
28
+ canonical attempt/receipt boundary. Standalone output cannot satisfy managed or
29
+ protected acceptance. Local-only implementation needs no forge selection or
30
+ scoped attempt. If remote delivery is requested,
31
+ use forge_server or forge_default: true, mutually exclusive; otherwise use the
32
+ repository remote. Preserve the first resolved server/repository/PR identity
33
+ throughout the assignment. Missing service or authorization yields a specific
34
+ blocker; this workflow does not invent grants or impersonate an integrator.
35
+
36
+ ## Project context loading
37
+
38
+ Read the task or inline scope and actual committed diff. In managed mode also
39
+ read assignment status, current attempt and retained delivery parameters. If a PR
40
+ is recorded, inspect it with
41
+ `ace-git pr show IDENTIFIER --format json --server NAME` using the retained selection
42
+ (or `--default-server`, otherwise remote resolution). Inside ACE use source
43
+ `bin/ace-*` commands. Load the relevant implementation and test instructions.
44
+
45
+ ## Standalone delivery
46
+
47
+ Implement and commit the requested local task/inline scope using existing task
48
+ and Git tools. Execute appropriate tests and independent review, retain their
49
+ actual artifact references and candidate SHA, and record unfinished criteria.
50
+ No managed assignment, protected service receipt or canonical qjl acceptance is
51
+ claimed by this route. Local scope may end here.
52
+
53
+ For an explicitly authorized standalone remote scope, use existing neutral
54
+ primitives on the chosen base repository/forge. Record actual head/base
55
+ repository URLs and refs; canonical provenance means matching repositories,
56
+ while fork provenance requires its explicit source URL. A pushed source ref must
57
+ match the candidate SHA. Draft creation defaults to true:
58
+
59
+ `ace-git pr create --server NAME --head HEAD-REF --head-repo HEAD-REPOSITORY-URL --base BASE-REF --expected-head CANDIDATE-SHA --title TITLE --body-file DESCRIPTION --format json`
60
+
61
+ Read the returned exact PR identity. Update with
62
+ `ace-git pr update IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --body-file DESCRIPTION --format json`.
63
+ Use the retained default/remote selection instead of `--server NAME` when
64
+ applicable. Review through the existing neutral review workflow with the same
65
+ selection. Before readiness or merge, require passed executed tests and accepted
66
+ independent review for that exact candidate and the actual operation authorization;
67
+ the low-level PR command does not establish these gates itself. Readiness uses
68
+ `ace-git pr ready IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --format json`.
69
+ Only an actually authorized standalone integrator may call
70
+ `ace-git pr merge IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --method METHOD --format json`.
71
+ This is not a worker fallback when a managed/protected service is unavailable.
72
+ Remote errors or uncertain effects remain explicit unresolved outcomes; use
73
+ authoritative reads and existing provider reconciliation, never blind replay.
74
+
75
+ Perform any requested local version/changelog changes before final test/review
76
+ and merge evidence is accepted. Preparation after a completed delivery is a
77
+ separate follow-up candidate/scope and must obtain its own required evidence.
78
+ Publication remains separately authorized and gated, including OTP when required.
79
+
80
+ ## Managed delivery steps
81
+
82
+ 1. Record the requested delivery steps and their owners before implementation.
83
+ Follow the existing task-work plan. Commit the implementation and run appropriate
84
+ package tests. If requested, perform local version/changelog preparation with
85
+ `ace-bundle wfi://release/local` before final evidence and merge; it does not
86
+ publish. Keep unfinished task criteria open; a bounded source checkpoint
87
+ cannot mark an entire installed task done.
88
+ 2. For remote delivery, prepare a concrete title and description from the changes
89
+ and executed validation. Load `ace-bundle wfi://git/pr/create` and use the actual
90
+ managed assignment and active delivery attempt. The executable operation is
91
+ `ace-assign delivery --assignment ID --attempt ATTEMPT --operation create --title TITLE --body-file DESCRIPTION`.
92
+ Provide `--parameters FILE` if required for the assignment's explicit delivery
93
+ mapping. Creation defaults to a draft. Record its returned server, repositories,
94
+ refs, PR number/URL and exact candidate_head.
95
+ 3. Execute the independent review workflow `ace-bundle wfi://review/pr` against that
96
+ exact PR and retained forge selection. Preserve its actual round completion rule,
97
+ findings and reviewer identities. Fix confirmed blocking findings and rerun
98
+ affected tests. Every new committed candidate, including task-only edits,
99
+ invalidates prior ready/merge evidence. Historical approval remains history.
100
+ 4. Obtain coordinator-accepted current-head test and independent review receipts
101
+ through the existing assignment boundary. File existence, fabricated approval,
102
+ exit zero or a worker claim cannot replace acceptance. Distinguish base_head,
103
+ candidate_head, evidence_git_ref and journal_commit; evidence commits do not
104
+ advance the deliverable branch. CI status is visible and advisory; failed
105
+ executed tests, stale evidence or unresolved review findings still block.
106
+ 5. Prepare the final PR body with the concrete behavior, executed checks,
107
+ independent verdict and remaining limitations. Load
108
+ `ace-bundle wfi://git/pr/update` and execute
109
+ `ace-assign delivery --assignment ID --attempt ATTEMPT --operation update --body-file DESCRIPTION`.
110
+ Mark ready only with accepted exact-head reference files:
111
+ `ace-assign delivery --assignment ID --attempt ATTEMPT --operation ready --tests TEST-REF --review REVIEW-REF`.
112
+ Each reference file contains only attempt_id and receipt_digest.
113
+ 6. Hand off merge only to the existing authorized integrator/service for the exact
114
+ PR, candidate and operation. Its executor uses the neutral provider merge with
115
+ both required values:
116
+ `ace-git pr merge IDENTIFIER --server NAME --expected-head CANDIDATE-SHA --method METHOD --format json`.
117
+ METHOD is the authorized squash, merge or rebase choice, never an implicit
118
+ default. The assignment worker consumes a completed verified service receipt:
119
+ `ace-assign delivery --assignment ID --attempt ATTEMPT --operation merge --tests TEST-REF --review REVIEW-REF --service-request REQUEST`.
120
+ This command does not dispatch or execute its own pending merge. If no actual
121
+ configured executor or valid receipt is available, retain the routed blocker.
122
+ 7. Local version/changelog preparation is optional requested work from step1.
123
+ It must be included before final exact-head evidence and merge. Preparation
124
+ requested after a completed merge starts a separate follow-up candidate/scope;
125
+ the old merge does not accept its amended content.
126
+ Publication, deployment and synchronization are distinct operations with their
127
+ own valid authorization, executed technical evidence and any required OTP.
128
+ Authorized merge does not authorize them. Do not add an unnecessary proposal
129
+ for an already authorized operation or fabricate an unavailable proposal API.
130
+ 8. Record the exact delivered scope, candidate, receipt references, review verdict
131
+ and remaining gates. A requested delivery-only scope can finish after its
132
+ authorized merge; publication is not mandatory. Keep the task in progress until
133
+ all of its actual acceptance criteria are satisfied.
134
+
135
+ ## Uncertainty and resume
136
+
137
+ In managed mode, read assignment status and retained qjl delivery events before
138
+ resuming. Repeat read/check operations safely. On interrupted create, update or readiness, use the
139
+ same delivery command for exact-selector reconciliation. One matching draft may
140
+ be adopted; zero remains unresolved and multiple matches conflict. A different
141
+ requested effect cannot be reported complete merely because an earlier effect
142
+ was recovered.
143
+
144
+ Unknown merge/publication remains unknown pending its existing owner's
145
+ reconciliation. Never blindly replay an effect, substitute a generic PR result
146
+ for a service receipt or translate a blocker into direct gh/fj commands.
147
+
148
+ ## Success criteria
149
+
150
+ - The requested scope has real artifact, current test and independent review
151
+ evidence for its exact candidate.
152
+ - Remote identity and explicit provenance remain fixed with honest unresolved
153
+ outcomes preserved; managed mode records them in the existing attempt journal.
154
+ - Readiness and authorized integration consume current evidence; managed merge
155
+ additionally consumes exact service receipts. Standalone output cannot satisfy
156
+ managed/protected acceptance. CI alone neither grants nor vetoes acceptance.
157
+ - Optional publication remains separately authorized and unfinished installed
158
+ criteria remain visible.
@@ -38,6 +38,15 @@ module Ace
38
38
  manifest(provider)["prompts_dir"]
39
39
  end
40
40
 
41
+ def extensions_dir(provider)
42
+ manifest(provider)["extensions_dir"]
43
+ end
44
+
45
+ def package_root(provider)
46
+ manifest_dir = File.dirname(manifest(provider).fetch("_manifest_path"))
47
+ File.expand_path("../../..", manifest_dir)
48
+ end
49
+
41
50
  def manifests
42
51
  @manifests ||= begin
43
52
  all_paths = local_manifest_paths + installed_manifest_paths
@@ -26,6 +26,7 @@ module Ace
26
26
  "(#{result[:projected_skills]} skills, #{result[:updated_files]} updated, " \
27
27
  "#{result[:removed_entries]} removed)"
28
28
  line += format_prompts_summary(result) if result.key?(:relative_prompts_dir)
29
+ line += format_extensions_summary(result) if result.key?(:relative_extensions_dir)
29
30
  puts line
30
31
  end
31
32
  print_inventory_summary(results)
@@ -41,6 +42,12 @@ module Ace
41
42
  "#{result[:removed_prompt_entries]} removed)"
42
43
  end
43
44
 
45
+ def format_extensions_summary(result)
46
+ "; extensions #{result[:relative_extensions_dir]} " \
47
+ "(#{result[:projected_extensions]} files, #{result[:updated_extension_files]} updated, " \
48
+ "#{result[:removed_extension_entries]} removed)"
49
+ end
50
+
44
51
  def print_inventory_summary(results)
45
52
  source_breakdown = results.first&.fetch(:source_breakdown, {})
46
53
  return if source_breakdown.nil? || source_breakdown.empty?
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "json"
5
+ require "pathname"
6
+ require "tempfile"
4
7
  require "yaml"
5
8
 
6
9
  module Ace
@@ -8,6 +11,7 @@ module Ace
8
11
  module Organisms
9
12
  class ProviderSyncer
10
13
  PROJECTION_SOURCE_PREFIX = "ace-handbook-integration-"
14
+ EXTENSION_RECEIPT_NAME = ".ace-handbook-projection.json"
11
15
 
12
16
  attr_reader :project_root, :registry, :inventory, :prompt_inventory, :config
13
17
 
@@ -36,11 +40,12 @@ module Ace
36
40
  private
37
41
 
38
42
  def sync_provider(provider, skills:, source_breakdown:)
39
- skills_result = sync_skills(provider, skills: skills, source_breakdown: source_breakdown)
43
+ result = sync_skills(provider, skills: skills, source_breakdown: source_breakdown)
40
44
  prompts_result = sync_prompts(provider)
41
- return skills_result if prompts_result.nil?
42
-
43
- skills_result.merge(prompts_result)
45
+ result = result.merge(prompts_result) unless prompts_result.nil?
46
+ extensions_result = sync_extensions(provider)
47
+ result = result.merge(extensions_result) unless extensions_result.nil?
48
+ result
44
49
  end
45
50
 
46
51
  def sync_skills(provider, skills:, source_breakdown:)
@@ -171,6 +176,258 @@ module Ace
171
176
  stale.size
172
177
  end
173
178
 
179
+ # Extension assets project from a package's handbook/extensions/ tree
180
+ # into the provider manifest's extensions_dir. The target directory may
181
+ # hold user-authored extensions, so pruning is receipt-based: only files
182
+ # recorded in the projection receipt from a previous sync are removed.
183
+ def sync_extensions(provider)
184
+ extensions_dir = registry.extensions_dir(provider)
185
+ return nil if extensions_dir.nil? || extensions_dir.to_s.empty?
186
+
187
+ source_dir = File.join(registry.package_root(provider), "handbook", "extensions")
188
+ expected = extension_source_files(source_dir)
189
+ output_dir = File.join(project_root, extensions_dir)
190
+ FileUtils.mkdir_p(output_dir)
191
+
192
+ receipt = read_projection_receipt(output_dir, provider)
193
+ # Ownership requires a valid receipt from this projection explicitly
194
+ # listing the path. A missing, symlinked, or corrupt receipt leaves
195
+ # ownership unknown: any conflicting destination file is refused so
196
+ # user-authored extensions can never be overwritten on a guess.
197
+ owned_paths = receipt&.fetch("files", []) || []
198
+ reject_unowned_collisions(output_dir, expected.keys, owned_paths)
199
+
200
+ # Pruning happens only after the replacement projection committed:
201
+ # a failed upgrade must leave the previous installation usable.
202
+ stale_paths = receipt&.fetch("files", [])&.-(expected.keys) || []
203
+ updated_files = 0
204
+
205
+ # Resolve and validate every destination before writing anything: a
206
+ # failure mid-copy must never leave half a projection behind (files
207
+ # written before a later validation error would be unowned and wedge
208
+ # every retry).
209
+ output_paths = expected.each_with_object({}) do |(relative_path, source_path), map|
210
+ map[relative_path] = validate_projection_destination(output_dir, relative_path, extensions_dir)
211
+ end
212
+
213
+ newly_created = []
214
+ saved_originals = {}
215
+ failed_prunes = []
216
+ begin
217
+ output_paths.each do |relative_path, output_path|
218
+ source_path = expected.fetch(relative_path)
219
+ FileUtils.mkdir_p(File.dirname(output_path))
220
+ created = !File.exist?(output_path)
221
+ # Register before copying: a copy that fails partway (disk
222
+ # exhaustion) must still roll back the partial file. Overwritten
223
+ # files are receipt-owned, so rollback restores their previous
224
+ # content instead of deleting them.
225
+ saved_originals[output_path] = File.binread(output_path) unless created
226
+ newly_created << output_path if created
227
+ next if !created && FileUtils.compare_file(source_path, output_path)
228
+
229
+ FileUtils.cp(source_path, output_path)
230
+ updated_files += 1
231
+ end
232
+
233
+ removed_entries = prune_stale_extension_files(output_dir, stale_paths, failed_prunes, saved_originals)
234
+ # Stale files whose removal failed keep their ownership until a
235
+ # later sync manages to delete them.
236
+ write_projection_receipt(output_dir, provider, expected.keys + failed_prunes)
237
+ rescue
238
+ # Roll back so a retry never finds its own partial installation
239
+ # standing in the way as an unowned collision, and restore the
240
+ # previous content of overwritten and pruned files — the previous
241
+ # installation must stay complete and loadable.
242
+ newly_created.each do |path|
243
+ FileUtils.rm_f(path)
244
+ rescue
245
+ nil
246
+ end
247
+ saved_originals.each do |path, content|
248
+ # Pruned assets restore after pruning emptied and removed their
249
+ # directories; a path swapped to a symlink mid-sync is never
250
+ # written through.
251
+ next if File.symlink?(path)
252
+
253
+ FileUtils.mkdir_p(File.dirname(path))
254
+ File.binwrite(path, content)
255
+ rescue
256
+ nil
257
+ end
258
+ newly_created.each do |path|
259
+ remove_empty_parent_dirs(File.dirname(path), output_dir)
260
+ end
261
+ raise
262
+ end
263
+
264
+ {
265
+ relative_extensions_dir: extensions_dir,
266
+ projected_extensions: expected.size,
267
+ updated_extension_files: updated_files,
268
+ removed_extension_entries: removed_entries
269
+ }
270
+ end
271
+
272
+ # The destination directory may hold user-authored extensions, so a
273
+ # destination file that exists before the first ACE projection (or one
274
+ # the receipt does not own) is never overwritten: the sync fails with
275
+ # a visible error instead of destroying user files.
276
+ def reject_unowned_collisions(output_dir, expected_relative_paths, owned_paths)
277
+ expected_relative_paths.each do |relative_path|
278
+ next if owned_paths.include?(relative_path)
279
+
280
+ candidate = safe_projection_path(output_dir, relative_path)
281
+ next if candidate.nil? || !File.exist?(candidate)
282
+
283
+ raise StandardError,
284
+ "refusing to overwrite existing file #{candidate}; it is not claimed by a valid projection receipt " \
285
+ "(missing, symlinked, or corrupt receipts claim nothing). Move or remove the file — or repair or remove " \
286
+ "#{File.join(output_dir, EXTENSION_RECEIPT_NAME)} — then rerun `ace-handbook sync`."
287
+ end
288
+ end
289
+
290
+ # Resolve and fully validate a destination before any file is
291
+ # written: every ancestor must be a directory or creatable (no
292
+ # regular files, no symlinks), so a mid-copy failure can never leave
293
+ # a half-installed projection that wedges retries.
294
+ def validate_projection_destination(output_dir, relative_path, extensions_dir)
295
+ segments = relative_path.split(File::SEPARATOR)
296
+ if segments.empty? || segments.any? { |segment| segment.empty? || segment == "." || segment == ".." }
297
+ raise StandardError, "cannot project #{relative_path.inspect}: invalid destination path"
298
+ end
299
+
300
+ current = Pathname.new(File.realpath(output_dir))
301
+ segments.each do |segment|
302
+ current = current.join(segment)
303
+ if File.symlink?(current.to_s)
304
+ raise StandardError,
305
+ "cannot project #{relative_path} into #{extensions_dir}: a symlinked path component would escape the projection directory"
306
+ end
307
+ next if segments.last == segment
308
+
309
+ if !current.directory? && File.exist?(current.to_s)
310
+ raise StandardError,
311
+ "cannot project #{relative_path} into #{extensions_dir}: #{current} exists and is not a directory"
312
+ end
313
+ end
314
+ if File.directory?(current.to_s)
315
+ # Receipt ownership of a path never authorizes writing through a
316
+ # directory planted at that path — cp would reinterpret the
317
+ # destination as a container and overwrite unowned content.
318
+ raise StandardError,
319
+ "cannot project #{relative_path} into #{extensions_dir}: #{current} exists and is a directory"
320
+ end
321
+ current.to_s
322
+ rescue Errno::ENOENT
323
+ raise StandardError, "cannot project #{relative_path} into #{extensions_dir}: projection directory vanished"
324
+ end
325
+
326
+ def extension_source_files(source_dir)
327
+ return {} unless Dir.exist?(source_dir)
328
+
329
+ source_root = Pathname.new(source_dir)
330
+ Dir.glob(File.join(source_dir, "**", "*"))
331
+ .select { |path| File.file?(path) }
332
+ .to_h { |path| [Pathname.new(path).relative_path_from(source_root).to_s, path] }
333
+ end
334
+
335
+ def prune_stale_extension_files(output_dir, stale_relative_paths, failed_prunes, rollback_ledger)
336
+ stale = stale_relative_paths
337
+ removed = 0
338
+ stale.each do |relative_path|
339
+ contained = safe_projection_path(output_dir, relative_path)
340
+ next if contained.nil?
341
+ next unless File.file?(contained)
342
+
343
+ begin
344
+ # Each pruned asset joins the rollback ledger before deletion:
345
+ # if publication later fails, restoring only overwritten files
346
+ # would leave the retained previous entrypoint importing a
347
+ # module that no longer exists.
348
+ rollback_ledger[contained] = File.binread(contained)
349
+ FileUtils.rm(contained)
350
+ removed += 1
351
+ remove_empty_parent_dirs(File.dirname(contained), output_dir)
352
+ rescue
353
+ # Keep ownership of the undeletable file: the next sync retries
354
+ # the removal instead of forgetting the file belongs to ACE.
355
+ failed_prunes << relative_path
356
+ end
357
+ end
358
+ removed
359
+ end
360
+
361
+ # Receipt files are projected data, so treat them as untrusted, and
362
+ # destination paths are resolved on the real filesystem: only plain
363
+ # relative paths whose every component stays inside output_dir — with
364
+ # no symlinked component, which could point anywhere — may be written
365
+ # or pruned.
366
+ def safe_projection_path(output_dir, relative_path)
367
+ return nil unless relative_path.is_a?(String)
368
+ return nil if relative_path.include?("\0")
369
+
370
+ segments = relative_path.split(File::SEPARATOR)
371
+ return nil if segments.empty? || segments.any? { |segment| segment.empty? || segment == "." || segment == ".." }
372
+
373
+ current = Pathname.new(File.realpath(output_dir))
374
+ segments.each do |segment|
375
+ current = current.join(segment)
376
+ return nil if File.symlink?(current.to_s)
377
+ end
378
+ current.to_s
379
+ rescue Errno::ENOENT
380
+ nil
381
+ end
382
+
383
+ def read_projection_receipt(output_dir, provider)
384
+ receipt_path = File.join(output_dir, EXTENSION_RECEIPT_NAME)
385
+ return nil if File.symlink?(receipt_path)
386
+ return nil unless File.file?(receipt_path)
387
+
388
+ receipt = JSON.parse(File.read(receipt_path))
389
+ return nil unless receipt.is_a?(Hash) && receipt["files"].is_a?(Array)
390
+ # A receipt from another projector (or without provenance) claims
391
+ # nothing: only our own source marker authorizes overwrites and
392
+ # pruning.
393
+ return nil unless receipt["source"] == "#{PROJECTION_SOURCE_PREFIX}#{provider}"
394
+
395
+ receipt
396
+ rescue JSON::ParserError
397
+ nil
398
+ end
399
+
400
+ def write_projection_receipt(output_dir, provider, relative_paths)
401
+ receipt_path = File.join(output_dir, EXTENSION_RECEIPT_NAME)
402
+ # Create the temp file exclusively in the destination directory: a
403
+ # predictable, plainly-opened temp path could follow a planted
404
+ # symlink and truncate a file outside the projection. The rename
405
+ # then replaces any symlink at the receipt path itself.
406
+ temp = Tempfile.create([".ace-handbook-projection", ".tmp"], output_dir)
407
+ begin
408
+ temp.write(JSON.pretty_generate(
409
+ "source" => "#{PROJECTION_SOURCE_PREFIX}#{provider}",
410
+ "files" => relative_paths.sort
411
+ ))
412
+ temp.close
413
+ File.rename(temp.path, receipt_path)
414
+ ensure
415
+ temp.close
416
+ FileUtils.rm_f(temp.path)
417
+ end
418
+ end
419
+
420
+ def remove_empty_parent_dirs(dir, stop_dir)
421
+ until dir == stop_dir
422
+ FileUtils.rmdir(dir)
423
+ dir = File.dirname(dir)
424
+ end
425
+ rescue Errno::ENOTEMPTY, Errno::ENOENT
426
+ # Surviving siblings (or a vanished ancestor) stop the ascent; the
427
+ # sync must continue writing remaining assets and the receipt.
428
+ nil
429
+ end
430
+
174
431
  # The prompts dir is shared with user-authored templates, so only files
175
432
  # carrying an ACE integration provenance marker are ever pruned.
176
433
  def prune_stale_prompt_files(output_dir, expected_template_names)
@@ -189,7 +446,7 @@ module Ace
189
446
 
190
447
  frontmatter = YAML.safe_load(match[1], permitted_classes: [Date, Time], aliases: true)
191
448
  frontmatter.is_a?(Hash) ? frontmatter["source"].to_s : ""
192
- rescue StandardError
449
+ rescue
193
450
  ""
194
451
  end
195
452
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Handbook
5
- VERSION = "0.32.0"
5
+ VERSION = "0.34.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-handbook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.0
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-27 00:00:00.000000000 Z
10
+ date: 2026-10-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-config