ace-handbook 0.31.1 → 0.33.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/CHANGELOG.md +11 -0
- data/README.md +1 -1
- data/handbook/skills/as-handbook-manage-overrides/SKILL.md +34 -0
- data/handbook/workflow-instructions/handbook/manage-cookbooks.wf.md +1 -0
- data/handbook/workflow-instructions/handbook/manage-overrides.wf.md +142 -0
- data/lib/ace/handbook/atoms/provider_registry.rb +13 -0
- data/lib/ace/handbook/cli/commands/sync.rb +18 -3
- data/lib/ace/handbook/models/prompt_template.rb +26 -0
- data/lib/ace/handbook/organisms/prompt_template_inventory.rb +85 -0
- data/lib/ace/handbook/organisms/provider_syncer.rb +322 -1
- data/lib/ace/handbook/version.rb +1 -1
- data/lib/ace/handbook.rb +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a640260822cd7518266797ae62628ae62a4cae9f6a96ca7604ed274245ac2915
|
|
4
|
+
data.tar.gz: 9f7e5b5515524a15a2547a874f0324961bb6e38489026d16f316bf2358bed2b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b67c9b8c9ad1c7a21121cd2e8694da47bfd545b2142dee84dfbcff70917049a0cb634e2194b938079db3f3a0d128ed3de1ca7e6b65585e97900c669a86ffd1c2
|
|
7
|
+
data.tar.gz: ad70a65d103696d260014b279073266ef0beab0e79a2d54ac4e575bc3217a5c1d290623a132e9bebce9192e49b7e89c609b34944ea50c47f3889f2d724fafc45
|
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.33.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- 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)`.
|
|
14
|
+
|
|
15
|
+
## [0.32.0] - 2026-09-27
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Provider manifests may declare an optional `prompts_dir`; `ace-handbook sync` now projects canonical prompt templates from `ace-handbook-integration-*` packages (`handbook/prompts/*.md`) into that directory (idempotent writes; prunes only stale files carrying an `ace-handbook-integration-*` provenance marker, leaving user-authored templates untouched). Sync summary output gains a `; prompts <dir> (N prompts, U updated, R removed)` segment when a provider projects prompts.
|
|
19
|
+
- Shipped `wfi://handbook/manage-overrides` (with paired skill `as-handbook-manage-overrides`): canonical override documentation covering all overridable surfaces (workflows, cookbooks, guides), the source-priority resolution rule, skills' non-overridability ("override the workflow the skill calls"), the `.agents/` sync-projection status, and planted-competitor verification probes.
|
|
20
|
+
|
|
10
21
|
### Changed
|
|
11
22
|
- Tightened `wfi://release/rubygems-publish` baseline guidance with an OTP-burst publication contract (build all artifacts before OTP; push in ≤5 concurrent waves; defer metadata verification) for project overlays.
|
|
12
23
|
|
data/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
**Integrate with resource discovery and context loading** - pair with [ace-nav](../ace-support-nav) for workflow and resource discovery, [ace-bundle](../ace-bundle) for loading complete workflow instructions, and provider integrations (`ace-handbook-integration-*`) that can project canonical handbook skills into harness-native folders on request.
|
|
38
38
|
|
|
39
|
-
**Sync and inspect skill projections** - run `ace-handbook sync` to project canonical skills into `.agents/skills/` by default, or use `ace-handbook sync --provider PROVIDER` for harness-native folders such as `.codex/skills/`.
|
|
39
|
+
**Sync and inspect skill projections** - run `ace-handbook sync` to project canonical skills into `.agents/skills/` by default, or use `ace-handbook sync --provider PROVIDER` for harness-native folders such as `.codex/skills/`. Providers whose manifest also declares `prompts_dir` additionally receive canonical prompt templates from the matching integration package's `handbook/prompts/*.md` (projected flat into that directory; user-authored templates without an `ace-handbook-integration-*` provenance marker are never pruned).
|
|
40
40
|
|
|
41
41
|
**Extend handbook content in normal projects** - put project-specific workflows, guides, cookbooks, templates, and skills under `.ace-handbook/` and discover them with protocol URLs (`wfi://`, `guide://`, `cookbook://`, `tmpl://`, `skill://`). See [Usage Guide](docs/usage.md) for path conventions.
|
|
42
42
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: as-handbook-manage-overrides
|
|
3
|
+
description: Manage project-local overrides of ace-managed docs (workflows, cookbooks, guides) and verify resolution precedence
|
|
4
|
+
# bundle: wfi://handbook/manage-overrides
|
|
5
|
+
# context: no-fork
|
|
6
|
+
# agent: general-purpose
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools:
|
|
9
|
+
- Bash(ace-nav:*)
|
|
10
|
+
- Bash(ace-handbook:*)
|
|
11
|
+
- Bash(ace-bundle:*)
|
|
12
|
+
- Read
|
|
13
|
+
- Write
|
|
14
|
+
- Edit
|
|
15
|
+
- Glob
|
|
16
|
+
- LS
|
|
17
|
+
argument-hint: "[surface|uri] [action: plan|verify]"
|
|
18
|
+
last_modified: 2026-09-20
|
|
19
|
+
source: ace-handbook
|
|
20
|
+
integration:
|
|
21
|
+
targets:
|
|
22
|
+
- claude
|
|
23
|
+
- codex
|
|
24
|
+
- gemini
|
|
25
|
+
- opencode
|
|
26
|
+
- pi
|
|
27
|
+
providers: {}
|
|
28
|
+
skill:
|
|
29
|
+
kind: workflow
|
|
30
|
+
execution:
|
|
31
|
+
workflow: wfi://handbook/manage-overrides
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
Load and run `ace-bundle wfi://handbook/manage-overrides` in the current project, then follow the loaded workflow as the source of truth and execute it end-to-end instead of only summarizing it.
|
|
@@ -60,6 +60,7 @@ Brief description of what this cookbook accomplishes.
|
|
|
60
60
|
|
|
61
61
|
- Package-owned cookbooks (inside `ace-handbook`): `ace-handbook/handbook/cookbooks/`
|
|
62
62
|
- Project-local cookbooks (outside package scope): `.ace-handbook/cookbooks/`
|
|
63
|
+
- Project-local document overrides (workflows, cookbooks, guides) and resolution precedence are governed canonically by `wfi://handbook/manage-overrides`
|
|
63
64
|
|
|
64
65
|
## Process Steps
|
|
65
66
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
name: manage-overrides
|
|
4
|
+
title: Manage Project-Local Overrides
|
|
5
|
+
description: Canonical reference for ace-managed document override surfaces, resolution precedence, and verification probes
|
|
6
|
+
purpose: Documentation for ace-handbook/handbook/workflow-instructions/handbook/manage-overrides.wf.md
|
|
7
|
+
allowed-tools:
|
|
8
|
+
- Bash(ace-nav:*)
|
|
9
|
+
- Bash(ace-bundle:*)
|
|
10
|
+
- Bash(ace-handbook:*)
|
|
11
|
+
- Read
|
|
12
|
+
- Write
|
|
13
|
+
- Edit
|
|
14
|
+
ace-docs:
|
|
15
|
+
last-updated: 2026-09-20
|
|
16
|
+
last-checked: 2026-09-20
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Manage Project-Local Overrides
|
|
20
|
+
|
|
21
|
+
## Goal
|
|
22
|
+
|
|
23
|
+
Answer, from shipped documentation and tooling, the three override questions: which ace-managed documents can this
|
|
24
|
+
project override, where do the project-local files live, and who wins when a project-local file and a gem file share
|
|
25
|
+
the same name. This workflow is the canonical reference; `ace-nav resolve --why` makes the same precedence observable
|
|
26
|
+
per URI.
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
|
|
30
|
+
- Understanding of handbook asset conventions (workflows, cookbooks, guides, skills)
|
|
31
|
+
- `ace-nav` and `ace-handbook` available in the project bundle
|
|
32
|
+
|
|
33
|
+
## Override Surfaces
|
|
34
|
+
|
|
35
|
+
| Surface | Protocol | Project-local location | Overridable? | Winner of duplicates (shipped defaults) |
|
|
36
|
+
|---------|----------|------------------------|--------------|------------------------------------------|
|
|
37
|
+
| Workflows | `wfi://` | `.ace-handbook/workflow-instructions/<ns>/<name>.wf.md` | Yes | Project-local, when its source priority is lower than the gem source's (e.g. the shipped `local` source at priority 5 vs gem at 10) |
|
|
38
|
+
| Cookbooks | `cookbook://` | `.ace-handbook/cookbooks/<name>.cookbook.md` | Yes | The candidate from the source with the lower priority number. The shipped project-local default registers priority 20 and the gem source 10, so **the gem wins by default**; register a project cookbook source with priority < 10 to make project-local win |
|
|
39
|
+
| Guides | `guide://` | `handbook/guides/<name>.g.md` | Yes, with registration | Requires a project source registration (`.ace/nav/protocols/guide-sources/local.yml` → `handbook/guides`); with priority < 10 the project-local guide wins. Without registration the gem wins |
|
|
40
|
+
| Skills | `skill://` | none | **No** | Always the gem. Skills are thin layers; override the workflow the skill calls instead |
|
|
41
|
+
| `.agents/skills/` projection | — | written by `ace-handbook sync` | Never edit | Not a resolution source. Neutral `agents`-provider mirror regenerated (and stale entries pruned) on every sync; it does not ship |
|
|
42
|
+
|
|
43
|
+
## Resolution Rule
|
|
44
|
+
|
|
45
|
+
Sources for a protocol are scanned in ascending priority order (lower number = higher precedence); the first source
|
|
46
|
+
containing the resource wins and remaining duplicates are ignored. Registration defaults: project sources 10, user
|
|
47
|
+
sources 50, gem defaults 100 — an individual source registration may pin another value (this is why the shipped
|
|
48
|
+
`local` workflow source at priority 5 beats the gem at 10, while the shipped project-local cookbook source at 20 loses
|
|
49
|
+
to the gem at 10).
|
|
50
|
+
|
|
51
|
+
To answer "why did this candidate win?" for a concrete URI without planting probe files:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ace-nav resolve wfi://release/publish --why
|
|
55
|
+
# uri: wfi://release/publish
|
|
56
|
+
# rule: Candidates are tried in ascending source-priority order ...
|
|
57
|
+
# candidates (resolution order):
|
|
58
|
+
# 1. [priority 5] @local (project) → ... ← winner
|
|
59
|
+
# 2. [priority 10] @ace-handbook (gem-default) → ...
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Skills Are Not Overridable
|
|
63
|
+
|
|
64
|
+
Skills (`skill://`) are thin launch layers that load and execute a workflow; they carry no logic worth overriding.
|
|
65
|
+
Consequently no project-local skill source exists: competitors planted in `.ace-handbook/skills/` or `.agents/skills/`
|
|
66
|
+
are ignored and resolution always returns the gem file. The design rule is: **override the workflow the skill calls**
|
|
67
|
+
(under `.ace-handbook/workflow-instructions/`), not the skill. To change a skill's own wording, change the canonical
|
|
68
|
+
`handbook/skills/<name>/SKILL.md` in the owning gem and run `ace-handbook sync`.
|
|
69
|
+
|
|
70
|
+
## Process Steps
|
|
71
|
+
|
|
72
|
+
1. **Identify the surface** you need to override (workflow, cookbook, or guide) and its canonical gem path.
|
|
73
|
+
2. **Create the project-local file** at the surface's project-local location (see table), keeping the canonical
|
|
74
|
+
frontmatter and structure so tools keep resolving it.
|
|
75
|
+
3. **Ensure the source registration makes your file win:**
|
|
76
|
+
- workflows: keep `.ace/nav/protocols/wfi-sources/local.yml` priority below the gem source (shipped default: 5).
|
|
77
|
+
- cookbooks: register `.ace/nav/protocols/cookbook-sources/local.yml` with priority < 10 (the shipped
|
|
78
|
+
project-local default of 20 loses to the gem's 10).
|
|
79
|
+
- guides: register `.ace/nav/protocols/guide-sources/local.yml` pointing at `handbook/guides` with priority < 10;
|
|
80
|
+
without a registration the project-local file is never the winner.
|
|
81
|
+
4. **Verify precedence with `ace-nav resolve <uri> --why`:** your file must be listed first and marked
|
|
82
|
+
`← winner`. For a list of all duplicate candidates use `ace-nav list '<protocol>://<name>'`.
|
|
83
|
+
5. **Never override skills or the `.agents/` projection.** Point the skill at an overridden workflow, or edit the
|
|
84
|
+
canonical skill in its gem.
|
|
85
|
+
6. **Document the override** in the project's own agent guidance so maintainers know the file shadows a gem asset and
|
|
86
|
+
should be re-checked on gem upgrades.
|
|
87
|
+
|
|
88
|
+
## Success Criteria
|
|
89
|
+
|
|
90
|
+
- [ ] Each project-local override file lives at the canonical project-local path for its surface.
|
|
91
|
+
- [ ] `ace-nav resolve <uri> --why` shows the project-local candidate first, marked `← winner` (or, for skills, shows
|
|
92
|
+
the gem candidate only).
|
|
93
|
+
- [ ] No files were planted in `.ace-handbook/skills/` or `.agents/skills/` expecting resolution changes.
|
|
94
|
+
- [ ] `ace-handbook sync` runs clean after any canonical skill change.
|
|
95
|
+
|
|
96
|
+
## Validation Commands
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
ace-bundle wfi://handbook/manage-overrides
|
|
100
|
+
ace-nav resolve wfi://release/publish --why
|
|
101
|
+
ace-nav list 'cookbook://setup-starting-a-multi-ruby-gem-monorepo-with-ace'
|
|
102
|
+
ace-handbook sync
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Planted-Competitor Verification Probes
|
|
106
|
+
|
|
107
|
+
Executed against ace-handbook 0.31.1 / ace-support-nav 0.28.6 (2026-09-20). Plant a project-local competitor file,
|
|
108
|
+
run the resolve, inspect the first line of the winner, then remove the plant:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Workflows: project-local WINS
|
|
112
|
+
mkdir -p .ace-handbook/workflow-instructions/handbook
|
|
113
|
+
printf '# OVERRIDE\n' > .ace-handbook/workflow-instructions/handbook/manage-cookbooks.wf.md
|
|
114
|
+
ace-nav resolve wfi://handbook/manage-cookbooks # → .ace-handbook/... (project-local)
|
|
115
|
+
rm .ace-handbook/workflow-instructions/handbook/manage-cookbooks.wf.md
|
|
116
|
+
|
|
117
|
+
# Cookbooks: with shipped defaults the GEM wins (project-local source priority 20 > gem 10)
|
|
118
|
+
mkdir -p .ace-handbook/cookbooks
|
|
119
|
+
printf '# OVERRIDE\n' > .ace-handbook/cookbooks/setup-starting-a-multi-ruby-gem-monorepo-with-ace.cookbook.md
|
|
120
|
+
ace-nav resolve cookbook://setup-starting-a-multi-ruby-gem-monorepo-with-ace # → ace-handbook/handbook/... (gem)
|
|
121
|
+
ace-nav list 'cookbook://setup-starting-a-multi-ruby-gem-monorepo-with-ace' # both candidates listed
|
|
122
|
+
rm .ace-handbook/cookbooks/setup-starting-a-multi-ruby-gem-monorepo-with-ace.cookbook.md
|
|
123
|
+
|
|
124
|
+
# Skills: gem WINS even with competitors in both overlay locations
|
|
125
|
+
mkdir -p .ace-handbook/skills/as-review-pr .agents/skills/as-review-pr
|
|
126
|
+
printf '# OVERRIDE\n' > .ace-handbook/skills/as-review-pr/SKILL.md
|
|
127
|
+
printf '# OVERRIDE\n' > .agents/skills/as-review-pr/SKILL.md
|
|
128
|
+
ace-nav resolve skill://as-review-pr # → ace-review/handbook/skills/... (gem)
|
|
129
|
+
rm .ace-handbook/skills/as-review-pr/SKILL.md
|
|
130
|
+
rmdir .ace-handbook/skills/as-review-pr .ace-handbook/skills
|
|
131
|
+
ace-handbook sync # regenerates .agents/skills/ and prunes the plant
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Error Handling
|
|
135
|
+
|
|
136
|
+
- **Override silently loses:** inspect `ace-nav resolve <uri> --why`; the losing candidate's source priority is too
|
|
137
|
+
high — lower it in the source registration.
|
|
138
|
+
- **Guide override not discovered at all:** no project guide source is registered; add
|
|
139
|
+
`.ace/nav/protocols/guide-sources/local.yml` (see Process Step 3).
|
|
140
|
+
- **Skill edit appears to do nothing:** skills are not overridable; override the workflow the skill calls.
|
|
141
|
+
- **`.agents/skills/` edits keep disappearing:** expected — `ace-handbook sync` regenerates the projection and prunes
|
|
142
|
+
stale entries; edit canonical skills in the owning gem.
|
|
@@ -34,6 +34,19 @@ module Ace
|
|
|
34
34
|
manifest(provider).fetch("output_dir")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def prompts_dir(provider)
|
|
38
|
+
manifest(provider)["prompts_dir"]
|
|
39
|
+
end
|
|
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
|
+
|
|
37
50
|
def manifests
|
|
38
51
|
@manifests ||= begin
|
|
39
52
|
all_paths = local_manifest_paths + installed_manifest_paths
|
|
@@ -22,9 +22,12 @@ module Ace
|
|
|
22
22
|
return if quiet
|
|
23
23
|
|
|
24
24
|
results.each do |result|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
line = "synced #{result[:provider]} -> #{result[:relative_output_dir]} " \
|
|
26
|
+
"(#{result[:projected_skills]} skills, #{result[:updated_files]} updated, " \
|
|
27
|
+
"#{result[:removed_entries]} removed)"
|
|
28
|
+
line += format_prompts_summary(result) if result.key?(:relative_prompts_dir)
|
|
29
|
+
line += format_extensions_summary(result) if result.key?(:relative_extensions_dir)
|
|
30
|
+
puts line
|
|
28
31
|
end
|
|
29
32
|
print_inventory_summary(results)
|
|
30
33
|
rescue => e
|
|
@@ -33,6 +36,18 @@ module Ace
|
|
|
33
36
|
|
|
34
37
|
private
|
|
35
38
|
|
|
39
|
+
def format_prompts_summary(result)
|
|
40
|
+
"; prompts #{result[:relative_prompts_dir]} " \
|
|
41
|
+
"(#{result[:projected_prompts]} prompts, #{result[:updated_prompt_files]} updated, " \
|
|
42
|
+
"#{result[:removed_prompt_entries]} removed)"
|
|
43
|
+
end
|
|
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
|
+
|
|
36
51
|
def print_inventory_summary(results)
|
|
37
52
|
source_breakdown = results.first&.fetch(:source_breakdown, {})
|
|
38
53
|
return if source_breakdown.nil? || source_breakdown.empty?
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Handbook
|
|
5
|
+
module Models
|
|
6
|
+
class PromptTemplate
|
|
7
|
+
attr_reader :provider, :source_path, :frontmatter, :content
|
|
8
|
+
|
|
9
|
+
def initialize(provider:, source_path:, frontmatter:, content:)
|
|
10
|
+
@provider = provider
|
|
11
|
+
@source_path = source_path
|
|
12
|
+
@frontmatter = frontmatter
|
|
13
|
+
@content = content
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def name
|
|
17
|
+
File.basename(source_path, ".md")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def source
|
|
21
|
+
frontmatter["source"].to_s
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Handbook
|
|
7
|
+
module Organisms
|
|
8
|
+
# Discovers canonical provider prompt templates shipped by integration packages.
|
|
9
|
+
#
|
|
10
|
+
# Each integration package owns its provider's prompt templates in
|
|
11
|
+
# handbook/prompts/*.md; they project into the provider manifest's prompts_dir.
|
|
12
|
+
class PromptTemplateInventory
|
|
13
|
+
FRONTMATTER_PATTERN = /\A---\s*\n(.*?)\n---\s*\n?(.*)\z/m
|
|
14
|
+
PACKAGE_GLOB = "ace-handbook-integration-*"
|
|
15
|
+
GEM_NAME_PREFIX = "ace-handbook-integration-"
|
|
16
|
+
|
|
17
|
+
attr_reader :project_root
|
|
18
|
+
|
|
19
|
+
def initialize(project_root: Ace::Handbook.project_root, gem_roots: nil)
|
|
20
|
+
@project_root = project_root
|
|
21
|
+
@gem_roots = gem_roots
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def all
|
|
25
|
+
package_roots.filter_map { |root, provider| parse_templates(root, provider) }
|
|
26
|
+
.each_with_object({}) do |(provider, templates), index|
|
|
27
|
+
index[provider] = (index[provider] || []).concat(templates)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def for_provider(provider)
|
|
32
|
+
all.fetch(provider.to_s, [])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def package_roots
|
|
38
|
+
(local_package_roots + installed_package_roots).sort.uniq.filter_map do |root|
|
|
39
|
+
provider = package_provider(root)
|
|
40
|
+
next if provider.nil? || provider.empty?
|
|
41
|
+
|
|
42
|
+
[root, provider]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def local_package_roots
|
|
47
|
+
Dir.glob(File.join(project_root, PACKAGE_GLOB)).select { |path| File.directory?(path) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def installed_package_roots
|
|
51
|
+
return @gem_roots unless @gem_roots.nil?
|
|
52
|
+
|
|
53
|
+
Gem::Specification.find_all
|
|
54
|
+
.select { |spec| spec.name.start_with?(GEM_NAME_PREFIX) }
|
|
55
|
+
.map(&:full_gem_path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def package_provider(root)
|
|
59
|
+
manifests = Dir.glob(File.join(root, ".ace-defaults", "handbook", "providers", "*.yml")).sort
|
|
60
|
+
return nil if manifests.empty?
|
|
61
|
+
|
|
62
|
+
data = YAML.safe_load_file(manifests.first, permitted_classes: [Date, Time], aliases: true) || {}
|
|
63
|
+
data["provider"].to_s
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_templates(root, provider)
|
|
69
|
+
templates = Dir.glob(File.join(root, "handbook", "prompts", "*.md")).sort.filter_map do |path|
|
|
70
|
+
match = File.read(path).match(FRONTMATTER_PATTERN)
|
|
71
|
+
frontmatter = match ? parse_frontmatter(match[1]) : {}
|
|
72
|
+
Models::PromptTemplate.new(provider: provider, source_path: path, frontmatter: frontmatter, content: File.read(path))
|
|
73
|
+
end
|
|
74
|
+
[provider, templates]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_frontmatter(raw)
|
|
78
|
+
YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: true) || {}
|
|
79
|
+
rescue StandardError
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
require "yaml"
|
|
4
8
|
|
|
5
9
|
module Ace
|
|
6
10
|
module Handbook
|
|
7
11
|
module Organisms
|
|
8
12
|
class ProviderSyncer
|
|
9
|
-
|
|
13
|
+
PROJECTION_SOURCE_PREFIX = "ace-handbook-integration-"
|
|
14
|
+
EXTENSION_RECEIPT_NAME = ".ace-handbook-projection.json"
|
|
15
|
+
|
|
16
|
+
attr_reader :project_root, :registry, :inventory, :prompt_inventory, :config
|
|
10
17
|
|
|
11
18
|
def initialize(
|
|
12
19
|
project_root: Ace::Handbook.project_root,
|
|
13
20
|
registry: nil,
|
|
14
21
|
inventory: nil,
|
|
22
|
+
prompt_inventory: nil,
|
|
15
23
|
config: nil
|
|
16
24
|
)
|
|
17
25
|
@project_root = project_root
|
|
18
26
|
@registry = registry || Atoms::ProviderRegistry.new(project_root: project_root)
|
|
19
27
|
@inventory = inventory || SkillInventory.new(project_root: project_root)
|
|
28
|
+
@prompt_inventory = prompt_inventory || PromptTemplateInventory.new(project_root: project_root)
|
|
20
29
|
@config = config || Ace::Handbook.config.resolve_namespace("handbook").to_h
|
|
21
30
|
end
|
|
22
31
|
|
|
@@ -31,6 +40,15 @@ module Ace
|
|
|
31
40
|
private
|
|
32
41
|
|
|
33
42
|
def sync_provider(provider, skills:, source_breakdown:)
|
|
43
|
+
result = sync_skills(provider, skills: skills, source_breakdown: source_breakdown)
|
|
44
|
+
prompts_result = sync_prompts(provider)
|
|
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
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def sync_skills(provider, skills:, source_breakdown:)
|
|
34
52
|
output_dir = File.join(project_root, registry.output_dir(provider))
|
|
35
53
|
prepare_output_dir(output_dir)
|
|
36
54
|
|
|
@@ -64,6 +82,35 @@ module Ace
|
|
|
64
82
|
}
|
|
65
83
|
end
|
|
66
84
|
|
|
85
|
+
def sync_prompts(provider)
|
|
86
|
+
prompts_dir = registry.prompts_dir(provider)
|
|
87
|
+
return nil if prompts_dir.nil? || prompts_dir.to_s.empty?
|
|
88
|
+
|
|
89
|
+
output_dir = File.join(project_root, prompts_dir)
|
|
90
|
+
FileUtils.mkdir_p(output_dir)
|
|
91
|
+
|
|
92
|
+
expected = {}
|
|
93
|
+
updated_files = 0
|
|
94
|
+
|
|
95
|
+
prompt_inventory.for_provider(provider).each do |template|
|
|
96
|
+
output_path = File.join(output_dir, "#{template.name}.md")
|
|
97
|
+
expected[template.name] = output_path
|
|
98
|
+
next if File.exist?(output_path) && File.read(output_path) == template.content
|
|
99
|
+
|
|
100
|
+
File.write(output_path, template.content)
|
|
101
|
+
updated_files += 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
removed_entries = prune_stale_prompt_files(output_dir, expected.keys)
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
relative_prompts_dir: prompts_dir,
|
|
108
|
+
projected_prompts: expected.size,
|
|
109
|
+
updated_prompt_files: updated_files,
|
|
110
|
+
removed_prompt_entries: removed_entries
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
67
114
|
def summarize_sources(skills)
|
|
68
115
|
skills.each_with_object(Hash.new(0)) do |skill, memo|
|
|
69
116
|
source = skill.source.to_s
|
|
@@ -128,6 +175,280 @@ module Ace
|
|
|
128
175
|
stale.each { |path| FileUtils.rm_rf(path) }
|
|
129
176
|
stale.size
|
|
130
177
|
end
|
|
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
|
+
|
|
431
|
+
# The prompts dir is shared with user-authored templates, so only files
|
|
432
|
+
# carrying an ACE integration provenance marker are ever pruned.
|
|
433
|
+
def prune_stale_prompt_files(output_dir, expected_template_names)
|
|
434
|
+
stale = Dir.glob(File.join(output_dir, "*.md")).reject do |path|
|
|
435
|
+
expected_template_names.include?(File.basename(path, ".md"))
|
|
436
|
+
end.select do |path|
|
|
437
|
+
template_source(path).start_with?(PROJECTION_SOURCE_PREFIX)
|
|
438
|
+
end
|
|
439
|
+
stale.each { |path| FileUtils.rm(path) }
|
|
440
|
+
stale.size
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def template_source(path)
|
|
444
|
+
match = File.read(path).match(/\A---\s*\n(.*?)\n---/m)
|
|
445
|
+
return "" unless match
|
|
446
|
+
|
|
447
|
+
frontmatter = YAML.safe_load(match[1], permitted_classes: [Date, Time], aliases: true)
|
|
448
|
+
frontmatter.is_a?(Hash) ? frontmatter["source"].to_s : ""
|
|
449
|
+
rescue
|
|
450
|
+
""
|
|
451
|
+
end
|
|
131
452
|
end
|
|
132
453
|
end
|
|
133
454
|
end
|
data/lib/ace/handbook/version.rb
CHANGED
data/lib/ace/handbook.rb
CHANGED
|
@@ -5,8 +5,10 @@ require "ace/support/config"
|
|
|
5
5
|
require_relative "handbook/version"
|
|
6
6
|
require_relative "handbook/atoms/provider_registry"
|
|
7
7
|
require_relative "handbook/models/skill_document"
|
|
8
|
+
require_relative "handbook/models/prompt_template"
|
|
8
9
|
require_relative "handbook/molecules/skill_projection"
|
|
9
10
|
require_relative "handbook/organisms/skill_inventory"
|
|
11
|
+
require_relative "handbook/organisms/prompt_template_inventory"
|
|
10
12
|
require_relative "handbook/organisms/provider_syncer"
|
|
11
13
|
require_relative "handbook/organisms/status_collector"
|
|
12
14
|
|
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.
|
|
4
|
+
version: 0.33.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-config
|
|
@@ -169,6 +169,7 @@ files:
|
|
|
169
169
|
- handbook/skills/as-handbook-manage-agents/SKILL.md
|
|
170
170
|
- handbook/skills/as-handbook-manage-cookbooks/SKILL.md
|
|
171
171
|
- handbook/skills/as-handbook-manage-guides/SKILL.md
|
|
172
|
+
- handbook/skills/as-handbook-manage-overrides/SKILL.md
|
|
172
173
|
- handbook/skills/as-handbook-manage-workflows/SKILL.md
|
|
173
174
|
- handbook/skills/as-handbook-parallel-research/SKILL.md
|
|
174
175
|
- handbook/skills/as-handbook-perform-delivery/SKILL.md
|
|
@@ -188,6 +189,7 @@ files:
|
|
|
188
189
|
- handbook/workflow-instructions/handbook/manage-agents.wf.md
|
|
189
190
|
- handbook/workflow-instructions/handbook/manage-cookbooks.wf.md
|
|
190
191
|
- handbook/workflow-instructions/handbook/manage-guides.wf.md
|
|
192
|
+
- handbook/workflow-instructions/handbook/manage-overrides.wf.md
|
|
191
193
|
- handbook/workflow-instructions/handbook/manage-workflows.wf.md
|
|
192
194
|
- handbook/workflow-instructions/handbook/parallel-research.wf.md
|
|
193
195
|
- handbook/workflow-instructions/handbook/perform-delivery.wf.md
|
|
@@ -207,8 +209,10 @@ files:
|
|
|
207
209
|
- lib/ace/handbook/cli.rb
|
|
208
210
|
- lib/ace/handbook/cli/commands/status.rb
|
|
209
211
|
- lib/ace/handbook/cli/commands/sync.rb
|
|
212
|
+
- lib/ace/handbook/models/prompt_template.rb
|
|
210
213
|
- lib/ace/handbook/models/skill_document.rb
|
|
211
214
|
- lib/ace/handbook/molecules/skill_projection.rb
|
|
215
|
+
- lib/ace/handbook/organisms/prompt_template_inventory.rb
|
|
212
216
|
- lib/ace/handbook/organisms/provider_syncer.rb
|
|
213
217
|
- lib/ace/handbook/organisms/skill_inventory.rb
|
|
214
218
|
- lib/ace/handbook/organisms/status_collector.rb
|