ace-support-config 0.16.3 ā 0.18.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b56e7f280832462c646dce84cf5565c6ac719dfaa57e3ece2b9974f2561b0a4
|
|
4
|
+
data.tar.gz: 0bf487c765f752a14ea748f06c951a71a7aab45be83126dd42307da23e8cfebd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f2e4c7389588f2b2e894cfb7730e6a226dbbfa36d5b9ba8bd55943e52195f28baca4bc493fed748bfba2388f4e5dfc1824aca28220219adf353ad41e5c083fc
|
|
7
|
+
data.tar.gz: f58924e6b836b963734ef369f1d548cf36a61144a07dc2d0779ed00f4c7f548a7204992badab4be93299a6772bc67b51e20c37497a7c0e57a95329ef5f8483e5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,9 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.18.0] - 2026-08-13
|
|
11
|
+
### Added
|
|
12
|
+
- Manage expiring drift acknowledgements
|
|
13
|
+
- Add profile-aware policy checks to doctor
|
|
14
|
+
- Report profile-aware configuration drift in doctor
|
|
15
|
+
### Technical
|
|
16
|
+
- Streamline bootstrap templates around principles-first guidance
|
|
17
|
+
|
|
18
|
+
## [0.17.1] - 2026-06-30
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Updated `ace-config doctor` skill projection health to ignore disabled provider projections reported by `ace-handbook status`.
|
|
22
|
+
|
|
23
|
+
## [0.17.0] - 2026-06-30
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Added `ace-config doctor` warnings for projects whose root agent guidance or `docs/tools.md` are missing generated agent-engineering guidance markers.
|
|
27
|
+
|
|
10
28
|
### Fixed
|
|
11
29
|
- Corrected `ace-config-bootstrap-root-files` to sync `ace-support-core` and record from the sandbox root layout where the bootstrap files are actually created.
|
|
12
30
|
|
|
31
|
+
### Technical
|
|
32
|
+
- Expanded bootstrap sync coverage for generated `docs/tools.md`, cost-bias markers, preservation without `--force`, force refresh, and subdirectory root targeting.
|
|
33
|
+
|
|
13
34
|
## [0.16.2] - 2026-04-24
|
|
14
35
|
|
|
15
36
|
### Technical
|
|
@@ -168,7 +168,19 @@ module Ace
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
def run_doctor(argv)
|
|
171
|
-
options = {
|
|
171
|
+
options = {
|
|
172
|
+
json: false,
|
|
173
|
+
no_probe: false,
|
|
174
|
+
probe: false,
|
|
175
|
+
hygiene: false,
|
|
176
|
+
verbose: false,
|
|
177
|
+
no_color: false,
|
|
178
|
+
quiet: false,
|
|
179
|
+
recommendations: false,
|
|
180
|
+
profile: nil,
|
|
181
|
+
strict: false,
|
|
182
|
+
check_updates: false
|
|
183
|
+
}
|
|
172
184
|
|
|
173
185
|
parser = OptionParser.new do |opts|
|
|
174
186
|
opts.banner = <<~BANNER.chomp
|
|
@@ -181,6 +193,10 @@ module Ace
|
|
|
181
193
|
OPTIONS
|
|
182
194
|
BANNER
|
|
183
195
|
opts.on("--json", "Output checks as JSON") { options[:json] = true }
|
|
196
|
+
opts.on("--recommendations", "Enable recommendation mode") { options[:recommendations] = true }
|
|
197
|
+
opts.on("--profile PROFILE", "Select profile (minimal, application, ace-development)") { |p| options[:profile] = p }
|
|
198
|
+
opts.on("--strict", "Exit nonzero on blocker or warning recommendations") { options[:strict] = true }
|
|
199
|
+
opts.on("--check-updates", "Check for package/recommendation updates") { options[:check_updates] = true }
|
|
184
200
|
opts.on("--hygiene", "Show full hygiene findings") { options[:hygiene] = true }
|
|
185
201
|
opts.on("-v", "--verbose", "Show full diagnostic detail") { options[:verbose] = true }
|
|
186
202
|
opts.on("-q", "--quiet", "Suppress output; use exit status only") { options[:quiet] = true }
|
|
@@ -195,15 +211,28 @@ module Ace
|
|
|
195
211
|
|
|
196
212
|
parser.parse!(argv)
|
|
197
213
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
doctor = Organisms::SetupDoctor.new
|
|
215
|
+
|
|
216
|
+
if options[:recommendations]
|
|
217
|
+
exit_code = doctor.run_recommendations(
|
|
218
|
+
profile: options[:profile],
|
|
219
|
+
strict: options[:strict],
|
|
220
|
+
check_updates: options[:check_updates],
|
|
221
|
+
json: options[:json],
|
|
222
|
+
quiet: options[:quiet]
|
|
223
|
+
)
|
|
224
|
+
else
|
|
225
|
+
exit_code = doctor.run(
|
|
226
|
+
json: options[:json],
|
|
227
|
+
no_probe: options[:no_probe],
|
|
228
|
+
probe: options[:probe],
|
|
229
|
+
hygiene: options[:hygiene],
|
|
230
|
+
verbose: options[:verbose],
|
|
231
|
+
colors: !options[:no_color],
|
|
232
|
+
quiet: options[:quiet]
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
207
236
|
exit(exit_code) if exit_code.positive?
|
|
208
237
|
end
|
|
209
238
|
|
|
@@ -51,6 +51,56 @@ module Ace
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def self.format_recommendations(profile, findings, strict: false)
|
|
55
|
+
output = []
|
|
56
|
+
output << "\nš„ Configuration Recommendations (Profile: #{profile})"
|
|
57
|
+
output << "=" * 50
|
|
58
|
+
output << ""
|
|
59
|
+
|
|
60
|
+
# Separate acknowledged from visible findings
|
|
61
|
+
visible = findings.reject { |f| f[:severity] == "acknowledged" }
|
|
62
|
+
acknowledged_count = findings.count { |f| f[:severity] == "acknowledged" }
|
|
63
|
+
|
|
64
|
+
if visible.empty? && acknowledged_count.zero?
|
|
65
|
+
output << "ā
No recommendations found - configuration matches profile '#{profile}'."
|
|
66
|
+
elsif visible.empty?
|
|
67
|
+
output << "ā
All findings acknowledged (#{acknowledged_count} active)."
|
|
68
|
+
else
|
|
69
|
+
visible.each_with_index do |f, idx|
|
|
70
|
+
glyph = case f[:severity]
|
|
71
|
+
when "blocker" then "ā"
|
|
72
|
+
when "warning" then "ā ļø"
|
|
73
|
+
when "recommendation" then "š”"
|
|
74
|
+
else "ā¹ļø"
|
|
75
|
+
end
|
|
76
|
+
output << "#{idx + 1}. #{glyph} [#{f[:severity].upcase}] #{f[:id]}"
|
|
77
|
+
output << " Source: #{f[:resolved_source]} | Version: #{f[:version]}"
|
|
78
|
+
output << " Current: #{f[:current_value]}"
|
|
79
|
+
output << " Recommended: #{f[:recommended_value]}"
|
|
80
|
+
output << " Rationale: #{f[:rationale]}" if f[:rationale]
|
|
81
|
+
output << " Next Action: #{f[:next_action]}" if f[:next_action]
|
|
82
|
+
if f[:acknowledgement_expired] && f[:acknowledgement]
|
|
83
|
+
ack = f[:acknowledgement]
|
|
84
|
+
output << " Prior Acknowledgement Expired: #{ack["rationale"]} (by #{ack["actor"]})"
|
|
85
|
+
end
|
|
86
|
+
output << ""
|
|
87
|
+
end
|
|
88
|
+
if acknowledged_count.positive?
|
|
89
|
+
output << " (#{acknowledged_count} finding#{"s" if acknowledged_count > 1} suppressed by active acknowledgement#{"s" if acknowledged_count > 1})"
|
|
90
|
+
output << ""
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
output << "=" * 50
|
|
95
|
+
has_failures = findings.any? { |f| %w[blocker warning].include?(f[:severity]) }
|
|
96
|
+
if strict && has_failures
|
|
97
|
+
output << "\e[31mRecommendation check failed under strict mode\e[0m"
|
|
98
|
+
else
|
|
99
|
+
output << "\e[32mRecommendation check completed\e[0m"
|
|
100
|
+
end
|
|
101
|
+
output.join("\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
54
104
|
def initialize(results, hygiene:, verbose:, colors:)
|
|
55
105
|
@results = results
|
|
56
106
|
@hygiene = hygiene
|
|
@@ -28,6 +28,12 @@ module Ace
|
|
|
28
28
|
CORE_ROLES = %w[commit doctor].freeze
|
|
29
29
|
UTILITY_ROLE_GROUPS = %w[_utility _utility-lite].freeze
|
|
30
30
|
ROLE_REFERENCE_PATTERN = /\brole:([A-Za-z0-9_-]+)\b/
|
|
31
|
+
COST_BIAS_MARKER = "Cost Bias Override"
|
|
32
|
+
AGENT_ENGINEERING_ANCHOR = "docs/tools.md#agent-engineering-practices"
|
|
33
|
+
AGENT_ENGINEERING_HEADING = "## Agent Engineering Practices"
|
|
34
|
+
AGENT_ENGINEERING_NEXT_ACTION = "Run ace-config sync ace-support-core --force in generated projects, " \
|
|
35
|
+
"or manually add the Cost Bias Override line and docs/tools.md Agent Engineering Practices section " \
|
|
36
|
+
"in customized projects."
|
|
31
37
|
|
|
32
38
|
def run(json: false, no_probe: false, probe: false, hygiene: false, verbose: false, colors: true, quiet: false, io: $stdout)
|
|
33
39
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -43,6 +49,7 @@ module Ace
|
|
|
43
49
|
append_check(checks, discovery_check, stream: stream, io: io)
|
|
44
50
|
|
|
45
51
|
append_check(checks, check_config_defaults, stream: stream, io: io)
|
|
52
|
+
append_check(checks, check_agent_engineering_guidance, stream: stream, io: io)
|
|
46
53
|
|
|
47
54
|
provider_context = load_provider_context if package_check[:status] != BLOCKER
|
|
48
55
|
|
|
@@ -82,6 +89,223 @@ module Ace
|
|
|
82
89
|
health_blocking?(checks) ? 1 : 0
|
|
83
90
|
end
|
|
84
91
|
|
|
92
|
+
VALID_PROFILES = %w[minimal application ace-development].freeze
|
|
93
|
+
|
|
94
|
+
def run_recommendations(profile: nil, strict: false, check_updates: false, json: false, quiet: false, io: $stdout)
|
|
95
|
+
resolved_profile = resolve_profile(profile)
|
|
96
|
+
unless VALID_PROFILES.include?(resolved_profile)
|
|
97
|
+
io.puts "Error: Unknown profile '#{profile}'. Accepted profiles are: #{VALID_PROFILES.join(", ")}"
|
|
98
|
+
return 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
findings = evaluate_recommendations(profile: resolved_profile, check_updates: check_updates)
|
|
102
|
+
|
|
103
|
+
has_strict_failures = findings.any? { |f| %w[blocker warning].include?(f[:severity]) }
|
|
104
|
+
exit_code = (strict && has_strict_failures) ? 1 : 0
|
|
105
|
+
|
|
106
|
+
unless quiet
|
|
107
|
+
if json
|
|
108
|
+
result = {
|
|
109
|
+
schema_version: "1.0",
|
|
110
|
+
profile: resolved_profile,
|
|
111
|
+
strict: strict,
|
|
112
|
+
valid: !has_strict_failures,
|
|
113
|
+
findings: findings
|
|
114
|
+
}
|
|
115
|
+
io.puts JSON.pretty_generate(result)
|
|
116
|
+
else
|
|
117
|
+
io.puts Molecules::SetupDoctorReporter.format_recommendations(resolved_profile, findings, strict: strict)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
exit_code
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def resolve_profile(cli_profile)
|
|
125
|
+
return cli_profile if cli_profile && !cli_profile.to_s.strip.empty?
|
|
126
|
+
|
|
127
|
+
root = project_root
|
|
128
|
+
proj_config = [
|
|
129
|
+
File.join(root, ".ace", "config", "config.yml"),
|
|
130
|
+
File.join(root, ".ace", "config.yml")
|
|
131
|
+
].find { |f| File.exist?(f) }
|
|
132
|
+
|
|
133
|
+
if proj_config
|
|
134
|
+
begin
|
|
135
|
+
data = YAML.safe_load_file(proj_config, aliases: true)
|
|
136
|
+
p = data&.dig("profile") || data&.dig("config", "profile")
|
|
137
|
+
return p.to_s if p && !p.to_s.strip.empty?
|
|
138
|
+
rescue
|
|
139
|
+
# fallback
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
"minimal"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def evaluate_recommendations(profile:, check_updates: false)
|
|
147
|
+
findings = []
|
|
148
|
+
root = project_root
|
|
149
|
+
version = "0.38.0"
|
|
150
|
+
|
|
151
|
+
# Finding 1: Artifact Hygiene (all profiles)
|
|
152
|
+
gitignore_path = File.join(root, ".gitignore")
|
|
153
|
+
if !File.exist?(gitignore_path) || !gitignore_entry_present?(File.read(gitignore_path), ".ace-local/")
|
|
154
|
+
findings << {
|
|
155
|
+
id: "rec-artifact-hygiene",
|
|
156
|
+
severity: "blocker",
|
|
157
|
+
profile: profile,
|
|
158
|
+
evidence: ".ace-local/ is missing from .gitignore",
|
|
159
|
+
resolved_source: "project",
|
|
160
|
+
current_value: File.exist?(gitignore_path) ? "missing .ace-local/" : "missing .gitignore",
|
|
161
|
+
recommended_value: ".ace-local/ in .gitignore",
|
|
162
|
+
rationale: "Prevents committing transient agent state",
|
|
163
|
+
next_action: "Add .ace-local/ to .gitignore",
|
|
164
|
+
version: version
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Finding 2: Provider Package (all profiles)
|
|
169
|
+
installed = begin
|
|
170
|
+
Gem::Specification.find_all_by_name(PROVIDER_GEM).any?
|
|
171
|
+
rescue
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
unless installed
|
|
175
|
+
findings << {
|
|
176
|
+
id: "rec-provider-package",
|
|
177
|
+
severity: "warning",
|
|
178
|
+
profile: profile,
|
|
179
|
+
evidence: "#{PROVIDER_GEM} gem is not installed",
|
|
180
|
+
resolved_source: "package_default",
|
|
181
|
+
current_value: "not_installed",
|
|
182
|
+
recommended_value: "#{PROVIDER_GEM} installed",
|
|
183
|
+
rationale: "Required for LLM provider discovery and execution",
|
|
184
|
+
next_action: "gem install #{PROVIDER_GEM}",
|
|
185
|
+
version: version
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Finding 3: Profile declaration (minimal profile)
|
|
190
|
+
if profile == "minimal"
|
|
191
|
+
findings << {
|
|
192
|
+
id: "rec-profile-config",
|
|
193
|
+
severity: "info",
|
|
194
|
+
profile: profile,
|
|
195
|
+
evidence: "Project relies on minimal fallback profile",
|
|
196
|
+
resolved_source: "package_default",
|
|
197
|
+
current_value: "minimal (fallback)",
|
|
198
|
+
recommended_value: "explicit profile in .ace/config/config.yml",
|
|
199
|
+
rationale: "Explicit profile selection enables profile-aware lifecycle checks",
|
|
200
|
+
next_action: "Add 'profile: application' to .ace/config/config.yml",
|
|
201
|
+
version: version
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Finding 4: Profile-specific checks for application & ace-development
|
|
206
|
+
if %w[application ace-development].include?(profile)
|
|
207
|
+
wt_config = File.join(root, ".ace", "git", "worktree.yml")
|
|
208
|
+
unless File.exist?(wt_config)
|
|
209
|
+
findings << {
|
|
210
|
+
id: "rec-worktree-bootstrap",
|
|
211
|
+
severity: "warning",
|
|
212
|
+
profile: profile,
|
|
213
|
+
evidence: ".ace/git/worktree.yml is missing",
|
|
214
|
+
resolved_source: "package_default",
|
|
215
|
+
current_value: "not_configured",
|
|
216
|
+
recommended_value: "project worktree policy configured",
|
|
217
|
+
rationale: "#{profile} profile requires explicit worktree preparation policy",
|
|
218
|
+
next_action: "ace-git-worktree config init",
|
|
219
|
+
version: version
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Guidance check
|
|
224
|
+
agents_md = File.join(root, "AGENTS.md")
|
|
225
|
+
tools_md = File.join(root, "docs", "tools.md")
|
|
226
|
+
unless File.exist?(agents_md) && File.exist?(tools_md)
|
|
227
|
+
findings << {
|
|
228
|
+
id: "rec-agent-guidance",
|
|
229
|
+
severity: "warning",
|
|
230
|
+
profile: profile,
|
|
231
|
+
evidence: "Agent engineering guidance is incomplete or unanchored",
|
|
232
|
+
resolved_source: "project",
|
|
233
|
+
current_value: File.exist?(agents_md) ? "missing docs/tools.md" : "missing AGENTS.md",
|
|
234
|
+
recommended_value: "AGENTS.md and docs/tools.md present",
|
|
235
|
+
rationale: "Provides structured engineering principles and agent practice guidance",
|
|
236
|
+
next_action: "Run ace-config sync ace-support-core --force",
|
|
237
|
+
version: version
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Finding 5: Application profile policy checks (strict application workflow validation)
|
|
243
|
+
if profile == "application"
|
|
244
|
+
# Check worktree root path safety if config exists
|
|
245
|
+
wt_config = File.join(root, ".ace", "git", "worktree.yml")
|
|
246
|
+
if File.exist?(wt_config)
|
|
247
|
+
begin
|
|
248
|
+
wt_data = YAML.safe_load_file(wt_config, aliases: true)
|
|
249
|
+
root_p = wt_data&.dig("git", "worktree", "root_path") || wt_data&.dig("root_path")
|
|
250
|
+
if root_p && (root_p.start_with?("/") || root_p.include?(".."))
|
|
251
|
+
findings << {
|
|
252
|
+
id: "rec-worktree-policy",
|
|
253
|
+
severity: "warning",
|
|
254
|
+
profile: "application",
|
|
255
|
+
evidence: "Worktree root_path '#{root_p}' escapes repository boundary",
|
|
256
|
+
resolved_source: "project",
|
|
257
|
+
current_value: root_p,
|
|
258
|
+
recommended_value: ".ace-wt (repository-local)",
|
|
259
|
+
rationale: "Application profile requires repository-local or common-root worktrees",
|
|
260
|
+
next_action: "Set git.worktree.root_path to .ace-wt in .ace/git/worktree.yml",
|
|
261
|
+
version: version
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
rescue
|
|
265
|
+
# Ignore parse failure
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Finding 6: ACE-development profile exception handling
|
|
271
|
+
if profile == "ace-development"
|
|
272
|
+
# Under ace-development profile, broad pipelines (batch worktrees, retrospectives, releases) are expected
|
|
273
|
+
findings << {
|
|
274
|
+
id: "rec-dev-pipeline",
|
|
275
|
+
severity: "info",
|
|
276
|
+
profile: "ace-development",
|
|
277
|
+
evidence: "ACE-development profile active",
|
|
278
|
+
resolved_source: "project",
|
|
279
|
+
current_value: "ace-development",
|
|
280
|
+
recommended_value: "ace-development",
|
|
281
|
+
rationale: "Broad development pipeline capabilities (batch, fork, release, demo) are enabled and accepted",
|
|
282
|
+
next_action: "No action required",
|
|
283
|
+
version: version
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Opt-in update check
|
|
288
|
+
if check_updates
|
|
289
|
+
findings << {
|
|
290
|
+
id: "rec-update-check",
|
|
291
|
+
severity: "info",
|
|
292
|
+
profile: profile,
|
|
293
|
+
evidence: "Opt-in update check feed evaluated",
|
|
294
|
+
resolved_source: "network",
|
|
295
|
+
current_value: "up_to_date",
|
|
296
|
+
recommended_value: "up_to_date",
|
|
297
|
+
rationale: "Ensures package recommendation versions match upstream releases",
|
|
298
|
+
next_action: "No update required",
|
|
299
|
+
version: version
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Apply acknowledgements: suppress acknowledged findings, re-expose expired ones
|
|
304
|
+
apply_acknowledgements(findings, profile: profile, root: root)
|
|
305
|
+
|
|
306
|
+
findings
|
|
307
|
+
end
|
|
308
|
+
|
|
85
309
|
private
|
|
86
310
|
|
|
87
311
|
def check_artifact_hygiene
|
|
@@ -173,6 +397,68 @@ module Ace
|
|
|
173
397
|
)
|
|
174
398
|
end
|
|
175
399
|
|
|
400
|
+
def check_agent_engineering_guidance
|
|
401
|
+
root = project_root
|
|
402
|
+
root_guidance_paths = %w[AGENTS.md CLAUDE.md].map { |name| File.join(root, name) }
|
|
403
|
+
docs_path = File.join(root, "docs", "tools.md")
|
|
404
|
+
existing_root_guidance = root_guidance_paths.select { |path| File.exist?(path) }
|
|
405
|
+
docs_content = File.exist?(docs_path) ? File.read(docs_path) : nil
|
|
406
|
+
guidance_contents = existing_root_guidance.to_h { |path| [path, File.read(path)] }
|
|
407
|
+
|
|
408
|
+
unless existing_root_guidance.any? || docs_content
|
|
409
|
+
return check(
|
|
410
|
+
id: "agent-engineering-guidance",
|
|
411
|
+
kind: "health",
|
|
412
|
+
status: PASS,
|
|
413
|
+
message: "Agent engineering guidance not installed"
|
|
414
|
+
)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
findings = []
|
|
418
|
+
guidance_contents.each do |path, content|
|
|
419
|
+
next if content.include?(COST_BIAS_MARKER)
|
|
420
|
+
|
|
421
|
+
findings << "#{File.basename(path)} lacks #{COST_BIAS_MARKER}"
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
if docs_content.nil?
|
|
425
|
+
findings << "docs/tools.md is missing"
|
|
426
|
+
elsif !docs_content.include?(AGENT_ENGINEERING_HEADING)
|
|
427
|
+
findings << "docs/tools.md lacks #{AGENT_ENGINEERING_HEADING}"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
if guidance_contents.values.any? { |content| content.include?(AGENT_ENGINEERING_ANCHOR) } &&
|
|
431
|
+
(!docs_content || !docs_content.include?(AGENT_ENGINEERING_HEADING))
|
|
432
|
+
findings << "root guidance links #{AGENT_ENGINEERING_ANCHOR} but the anchor target is absent"
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
if findings.empty?
|
|
436
|
+
return check(
|
|
437
|
+
id: "agent-engineering-guidance",
|
|
438
|
+
kind: "health",
|
|
439
|
+
status: PASS,
|
|
440
|
+
message: "Agent engineering guidance is present"
|
|
441
|
+
)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
check(
|
|
445
|
+
id: "agent-engineering-guidance",
|
|
446
|
+
kind: "health",
|
|
447
|
+
status: WARN,
|
|
448
|
+
message: "Agent engineering guidance is incomplete",
|
|
449
|
+
next_action: AGENT_ENGINEERING_NEXT_ACTION,
|
|
450
|
+
details: findings.uniq
|
|
451
|
+
)
|
|
452
|
+
rescue => e
|
|
453
|
+
check(
|
|
454
|
+
id: "agent-engineering-guidance",
|
|
455
|
+
kind: "health",
|
|
456
|
+
status: WARN,
|
|
457
|
+
message: "Agent engineering guidance check failed: #{e.message}",
|
|
458
|
+
next_action: AGENT_ENGINEERING_NEXT_ACTION
|
|
459
|
+
)
|
|
460
|
+
end
|
|
461
|
+
|
|
176
462
|
def check_skill_sync
|
|
177
463
|
out, err, status = Open3.capture3("ace-handbook", "status", "--format", "json")
|
|
178
464
|
unless status.success?
|
|
@@ -188,7 +474,8 @@ module Ace
|
|
|
188
474
|
|
|
189
475
|
snapshot = JSON.parse(out)
|
|
190
476
|
providers = Array(snapshot["providers"])
|
|
191
|
-
|
|
477
|
+
checked_providers = providers.select { |entry| entry.fetch("enabled", true) }
|
|
478
|
+
drifted = checked_providers.select do |entry|
|
|
192
479
|
entry.fetch("missing", 0).to_i.positive? ||
|
|
193
480
|
entry.fetch("outdated", 0).to_i.positive? ||
|
|
194
481
|
entry.fetch("extra", 0).to_i.positive?
|
|
@@ -200,8 +487,8 @@ module Ace
|
|
|
200
487
|
id: "skill-sync",
|
|
201
488
|
kind: "health",
|
|
202
489
|
status: PASS,
|
|
203
|
-
message: "Provider skills are in sync (#{
|
|
204
|
-
skill_sync: {providers:
|
|
490
|
+
message: "Provider skills are in sync (#{checked_providers.length} providers, #{total_skills} skills)",
|
|
491
|
+
skill_sync: {providers: checked_providers, canonical_total: total_skills}
|
|
205
492
|
)
|
|
206
493
|
end
|
|
207
494
|
|
|
@@ -209,10 +496,10 @@ module Ace
|
|
|
209
496
|
id: "skill-sync",
|
|
210
497
|
kind: "health",
|
|
211
498
|
status: WARN,
|
|
212
|
-
message: "Provider skill sync drift detected (#{drifted.length}/#{
|
|
499
|
+
message: "Provider skill sync drift detected (#{drifted.length}/#{checked_providers.length} providers)",
|
|
213
500
|
next_action: "Run ace-handbook sync to refresh provider-native skills.",
|
|
214
501
|
details: drifted.map { |entry| skill_sync_detail(entry) },
|
|
215
|
-
skill_sync: {providers:
|
|
502
|
+
skill_sync: {providers: checked_providers, drifted: drifted}
|
|
216
503
|
)
|
|
217
504
|
rescue Errno::ENOENT
|
|
218
505
|
check(
|
|
@@ -1062,6 +1349,115 @@ module Ace
|
|
|
1062
1349
|
token = token.delete_suffix("/")
|
|
1063
1350
|
token
|
|
1064
1351
|
end
|
|
1352
|
+
|
|
1353
|
+
# --- Acknowledgement support ---
|
|
1354
|
+
|
|
1355
|
+
# Load acknowledgements from project config and apply them to findings.
|
|
1356
|
+
# Active valid acknowledgements change severity to "acknowledged".
|
|
1357
|
+
# Expired acknowledgements keep original severity but attach prior context.
|
|
1358
|
+
def apply_acknowledgements(findings, profile:, root:)
|
|
1359
|
+
acks = load_acknowledgements(root)
|
|
1360
|
+
return if acks.nil? || acks.empty?
|
|
1361
|
+
|
|
1362
|
+
now = Time.now
|
|
1363
|
+
|
|
1364
|
+
findings.each do |finding|
|
|
1365
|
+
ack = acks[finding[:id]]
|
|
1366
|
+
next unless ack.is_a?(Hash)
|
|
1367
|
+
|
|
1368
|
+
validity = validate_acknowledgement(ack, finding, profile, now)
|
|
1369
|
+
case validity
|
|
1370
|
+
when :active
|
|
1371
|
+
finding[:original_severity] = finding[:severity]
|
|
1372
|
+
finding[:severity] = "acknowledged"
|
|
1373
|
+
finding[:acknowledgement] = ack
|
|
1374
|
+
when :expired
|
|
1375
|
+
finding[:acknowledgement] = ack
|
|
1376
|
+
finding[:acknowledgement_expired] = true
|
|
1377
|
+
end
|
|
1378
|
+
# :invalid ā leave finding unchanged, ack is silently ignored
|
|
1379
|
+
end
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
# Load recommendation_acknowledgements from project config file.
|
|
1383
|
+
# Returns a Hash keyed by finding id, or empty Hash.
|
|
1384
|
+
def load_acknowledgements(root)
|
|
1385
|
+
config_paths = [
|
|
1386
|
+
File.join(root, ".ace", "config", "config.yml"),
|
|
1387
|
+
File.join(root, ".ace", "config.yml")
|
|
1388
|
+
]
|
|
1389
|
+
config_path = config_paths.find { |f| File.exist?(f) }
|
|
1390
|
+
return {} unless config_path
|
|
1391
|
+
|
|
1392
|
+
data = YAML.safe_load_file(config_path, permitted_classes: [Time, Date], aliases: true)
|
|
1393
|
+
data&.dig("recommendation_acknowledgements") || {}
|
|
1394
|
+
rescue StandardError
|
|
1395
|
+
{}
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
# Validate an acknowledgement against finding, profile, and time.
|
|
1399
|
+
# Returns :active, :expired, or :invalid.
|
|
1400
|
+
def validate_acknowledgement(ack, finding, profile, now)
|
|
1401
|
+
# Required fields
|
|
1402
|
+
rationale = ack["rationale"].to_s.strip
|
|
1403
|
+
return :invalid if rationale.empty?
|
|
1404
|
+
|
|
1405
|
+
actor = ack["actor"].to_s.strip
|
|
1406
|
+
return :invalid if actor.empty?
|
|
1407
|
+
|
|
1408
|
+
acknowledged_at = parse_time(ack["acknowledged_at"])
|
|
1409
|
+
return :invalid unless acknowledged_at
|
|
1410
|
+
|
|
1411
|
+
# Profile scope: if declared, must match
|
|
1412
|
+
ack_profile = ack["profile"].to_s.strip
|
|
1413
|
+
return :invalid if !ack_profile.empty? && ack_profile != profile
|
|
1414
|
+
|
|
1415
|
+
# Version scope: if declared, must match finding version
|
|
1416
|
+
ack_version = ack["recommendation_version"].to_s.strip
|
|
1417
|
+
if !ack_version.empty? && finding[:version]
|
|
1418
|
+
return :invalid unless version_matches?(ack_version, finding[:version])
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
# Time boundary: expires_at or recheck_after required
|
|
1422
|
+
expires_at = parse_time(ack["expires_at"])
|
|
1423
|
+
recheck_after = parse_time(ack["recheck_after"])
|
|
1424
|
+
return :invalid if expires_at.nil? && recheck_after.nil?
|
|
1425
|
+
|
|
1426
|
+
# Expiry check: at-the-instant is expired
|
|
1427
|
+
boundary = expires_at || recheck_after
|
|
1428
|
+
return :expired if now >= boundary
|
|
1429
|
+
|
|
1430
|
+
:active
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
# Parse a time value from string or Time object.
|
|
1434
|
+
# Returns Time or nil.
|
|
1435
|
+
def parse_time(value)
|
|
1436
|
+
return nil if value.nil?
|
|
1437
|
+
return value if value.is_a?(Time)
|
|
1438
|
+
return value.to_time if value.is_a?(Date)
|
|
1439
|
+
|
|
1440
|
+
Time.parse(value.to_s)
|
|
1441
|
+
rescue ArgumentError, TypeError
|
|
1442
|
+
nil
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
# Check if acknowledgement version constraint matches finding version.
|
|
1446
|
+
# Supports exact match and pessimistic (~>) constraints.
|
|
1447
|
+
def version_matches?(constraint, version)
|
|
1448
|
+
return true if constraint == version
|
|
1449
|
+
|
|
1450
|
+
if constraint.start_with?("~>")
|
|
1451
|
+
prefix = constraint.sub("~>", "").strip
|
|
1452
|
+
parts = prefix.split(".")
|
|
1453
|
+
return false if parts.length < 2
|
|
1454
|
+
|
|
1455
|
+
# Pessimistic: ~> 0.38 means >= 0.38.0 and < 1.0
|
|
1456
|
+
version.start_with?(parts[0...-1].join(".") + ".")
|
|
1457
|
+
else
|
|
1458
|
+
constraint == version
|
|
1459
|
+
end
|
|
1460
|
+
end
|
|
1065
1461
|
end
|
|
1066
1462
|
end
|
|
1067
1463
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-support-config
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-fs
|