ace-assign 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +51 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +266 -0
- data/docs/usage.md +91 -2
- data/handbook/workflow-instructions/assign/drive.wf.md +2 -2
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/delivery_parameters.rb +54 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +160 -0
- data/lib/ace/assign/authority/client.rb +107 -0
- data/lib/ace/assign/authority/deployment.rb +465 -0
- data/lib/ace/assign/authority/endcap.rb +342 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +314 -0
- data/lib/ace/assign/authority/endcap_results.rb +380 -0
- data/lib/ace/assign/authority/endcap_services.rb +395 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +476 -0
- data/lib/ace/assign/authority/launch_driver.rb +114 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +822 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +88 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +84 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +115 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +180 -0
- data/lib/ace/assign/authority/posix_acl.rb +56 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +54 -0
- data/lib/ace/assign/authority/server.rb +230 -0
- data/lib/ace/assign/authority/service_evidence.rb +55 -0
- data/lib/ace/assign/authority/transfer_codec.rb +155 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +45 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/create.rb +9 -1
- data/lib/ace/assign/cli/commands/delivery.rb +57 -0
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +36 -0
- data/lib/ace/assign/cli/commands/resume.rb +26 -0
- data/lib/ace/assign/cli.rb +22 -1
- data/lib/ace/assign/models/evidence_event.rb +3 -1
- data/lib/ace/assign/molecules/attempt_reconciler.rb +31 -12
- data/lib/ace/assign/molecules/canonical_evidence.rb +150 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +218 -63
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +327 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +202 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +4 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/receipt_verifier.rb +49 -19
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +4 -1
- data/lib/ace/assign/organisms/assignment_executor.rb +6 -0
- data/lib/ace/assign/organisms/attempt_coordinator.rb +312 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +8 -3
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +13 -0
- metadata +119 -6
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "yaml"
|
|
3
|
+
require "ace/git/organisms/pull_request_lifecycle"
|
|
4
|
+
require_relative "../atoms/delivery_parameters"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Assign
|
|
8
|
+
module Organisms
|
|
9
|
+
# Remote delivery is a projection of the existing attempt journal.
|
|
10
|
+
# Intents precede writes; an interrupted intent permits reconciliation
|
|
11
|
+
# reads only. Privileged merge is executed by qjx, never this worker.
|
|
12
|
+
class DeliveryCoordinator
|
|
13
|
+
def initialize(repo_root:, coordinator: nil, journal: nil, lifecycle_factory: nil, exclusion: nil, identity_resolver: nil)
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@identity_resolver = identity_resolver || Molecules::ExecutionIdentityResolver.new
|
|
16
|
+
@repo_root = repo_root
|
|
17
|
+
@journal = journal || Molecules::EvidenceJournal.new(repo_root: repo_root)
|
|
18
|
+
@coordinator = coordinator || AttemptCoordinator.new(repo_root: repo_root, journal: @journal)
|
|
19
|
+
@factory = lifecycle_factory || ->(selection) { Ace::Git::Organisms::PullRequestLifecycle.new(**selection) }
|
|
20
|
+
@exclusion = exclusion || Molecules::LifecycleExclusion.new(repo_root: repo_root)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def perform(**arguments)
|
|
24
|
+
@mutex.synchronize { execute_request(**arguments) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def execute_request(assignment_id:, attempt_id:, operation:, parameters: nil, title: nil, body: nil,
|
|
30
|
+
tests: nil, review: nil, service_request_id: nil)
|
|
31
|
+
@identity = @identity_resolver.resolve
|
|
32
|
+
unless @identity_resolver.trusted?(@identity)
|
|
33
|
+
raise AttemptErrors::UnauthorizedIdentity, "Only a trusted coordinator or service may accept delivery evidence"
|
|
34
|
+
end
|
|
35
|
+
raise ArgumentError, "Unsupported delivery operation" unless %w[create update ready review merge status].include?(operation)
|
|
36
|
+
if operation == "create" && (!title.is_a?(String) || title.strip.empty?)
|
|
37
|
+
raise ArgumentError, "Create requires a concrete PR title"
|
|
38
|
+
end
|
|
39
|
+
if operation == "update" && title.nil? && body.nil?
|
|
40
|
+
raise ArgumentError, "Update requires title or body"
|
|
41
|
+
end
|
|
42
|
+
raise ArgumentError, "PR body must be text" unless body.nil? || body.is_a?(String)
|
|
43
|
+
params = Atoms::DeliveryParameters.validate(parameters || assignment_parameters(assignment_id))
|
|
44
|
+
binding = {"assignment_id" => assignment_id, "attempt_id" => attempt_id}
|
|
45
|
+
attempt = @coordinator.authoritative_attempt(binding)
|
|
46
|
+
raise AttemptErrors::NotFound, "Delivery attempt not found" unless attempt
|
|
47
|
+
raise AttemptErrors::ReceiptRejected, "Delivery requires a managed attempt" unless attempt.managed?
|
|
48
|
+
@exclusion.with_shared(@exclusion.task_key(attempt.binding.task_id)) do
|
|
49
|
+
@exclusion.with_exclusive(@exclusion.assignment_key(assignment_id)) do
|
|
50
|
+
raise AttemptErrors::Conflict, "Assignment was pruned" if @exclusion.removed?(@exclusion.assignment_key(assignment_id))
|
|
51
|
+
execute(binding, operation, params, title: title, body: body, tests: tests, review: review,
|
|
52
|
+
service_request_id: service_request_id)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def assignment_parameters(assignment_id)
|
|
60
|
+
assignment = Molecules::AssignmentManager.new.load(assignment_id)
|
|
61
|
+
raise AttemptErrors::NotFound, "Assignment not found" unless assignment
|
|
62
|
+
document = YAML.safe_load_file(assignment.source_config, permitted_classes: [Time, Date])
|
|
63
|
+
document.fetch("delivery")
|
|
64
|
+
rescue KeyError, Errno::ENOENT, Psych::Exception
|
|
65
|
+
raise ArgumentError, "Assignment needs explicit delivery parameters"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def execute(binding, operation, params, **options)
|
|
69
|
+
@binding = binding
|
|
70
|
+
@intent_context = nil
|
|
71
|
+
@attempt = @coordinator.authoritative_attempt(binding)
|
|
72
|
+
unless @attempt&.active? && @attempt.managed?
|
|
73
|
+
raise AttemptErrors::ReceiptRejected, "Delivery attempt is no longer active"
|
|
74
|
+
end
|
|
75
|
+
@head = git!("rev-parse", "HEAD").strip
|
|
76
|
+
selection = {server_name: params["forge_server"], use_default: params["forge_default"], repo_root: @repo_root}
|
|
77
|
+
historical = events.find { |event| event.dig("payload", "stage") == "identity" }&.fetch("payload")
|
|
78
|
+
selection = {server_name: historical.dig("server", "name"), use_default: false, repo_root: @repo_root} if historical
|
|
79
|
+
@lifecycle = @factory.call(selection)
|
|
80
|
+
server = @lifecycle.resolved_identity
|
|
81
|
+
@server = server
|
|
82
|
+
Atoms::DeliveryParameters.validate_url!(server.fetch("url"))
|
|
83
|
+
provenance = params.fetch("pr_provenance")
|
|
84
|
+
unless Ace::Git::Atoms::ServerUrl.match?(server["url"], provenance["base_repository_url"])
|
|
85
|
+
raise Ace::Git::ProviderIdentityMismatchError, "Selected forge does not match delivery base"
|
|
86
|
+
end
|
|
87
|
+
if historical
|
|
88
|
+
unless historical["server"] == server && historical["provenance"] == provenance &&
|
|
89
|
+
historical["selection"] == params.slice("forge_server", "forge_default")
|
|
90
|
+
raise AttemptErrors::Conflict, "Delivery identity or selection changed during attempt"
|
|
91
|
+
end
|
|
92
|
+
elsif !%w[status review].include?(operation)
|
|
93
|
+
record("stage" => "identity", "server" => server, "provenance" => provenance,
|
|
94
|
+
"selection" => params.slice("forge_server", "forge_default"))
|
|
95
|
+
end
|
|
96
|
+
@provenance = provenance
|
|
97
|
+
pinned = Ace::Git::ResolvedServer.new(name: server.fetch("name"),
|
|
98
|
+
provider: server.fetch("provider").to_sym, url: server.fetch("url"))
|
|
99
|
+
@lifecycle = @factory.call(server_name: pinned.name, use_default: false,
|
|
100
|
+
repo_root: @repo_root, resolved_server: pinned)
|
|
101
|
+
verify_pushed_head!
|
|
102
|
+
return status if operation == "status"
|
|
103
|
+
return review_snapshot if operation == "review"
|
|
104
|
+
pending = unresolved_intent
|
|
105
|
+
if pending
|
|
106
|
+
result = reconcile(pending)
|
|
107
|
+
return result if same_effect?(pending, operation, options)
|
|
108
|
+
raise AttemptErrors::CurrentEffectRequired.new(operation: operation, recovered_operation: pending["operation"])
|
|
109
|
+
end
|
|
110
|
+
require_evidence!(options[:tests], options[:review]) if %w[ready merge].include?(operation)
|
|
111
|
+
return consume_merge(options[:service_request_id]) if operation == "merge"
|
|
112
|
+
pr = current_pr
|
|
113
|
+
if operation == "create" && pr
|
|
114
|
+
requested = content_input(options)
|
|
115
|
+
metadata = @lifecycle.review_metadata_snapshot(pr.number).pull_request
|
|
116
|
+
verify_pr!(metadata)
|
|
117
|
+
unless content_matches?(requested.keys.map(&:to_s), Atoms::EvidenceDigest.digest(requested), metadata)
|
|
118
|
+
raise AttemptErrors::CurrentEffectRequired.new(operation: operation, recovered_operation: "create")
|
|
119
|
+
end
|
|
120
|
+
return reconcile({"operation" => "create", "head" => @head,
|
|
121
|
+
"fields" => requested.keys.map(&:to_s), "input_digest" => Atoms::EvidenceDigest.digest(requested)})
|
|
122
|
+
end
|
|
123
|
+
raise AttemptErrors::ReceiptRejected, "Create a draft before #{operation}" if operation != "create" && !pr
|
|
124
|
+
verify_pr!(pr) if pr
|
|
125
|
+
intent = {"stage" => "intent", "operation" => operation, "head" => @head,
|
|
126
|
+
"input_digest" => Atoms::EvidenceDigest.digest(content_input(options)),
|
|
127
|
+
"fields" => content_input(options).keys.map(&:to_s),
|
|
128
|
+
"pr_number" => pr&.number, "tests" => options[:tests], "review" => options[:review]}
|
|
129
|
+
record(intent)
|
|
130
|
+
@intent_context = unresolved_intent
|
|
131
|
+
receipt = case operation
|
|
132
|
+
when "create"
|
|
133
|
+
@lifecycle.create(head_ref: provenance["head_ref"], base_ref: provenance["base_ref"],
|
|
134
|
+
head_repository_url: provenance["head_repository_url"], expected_head: @head,
|
|
135
|
+
title: options[:title], body: options[:body], draft: true)
|
|
136
|
+
when "update"
|
|
137
|
+
@lifecycle.update(pr.number, expected_head: @head, title: options[:title], body: options[:body])
|
|
138
|
+
when "ready" then @lifecycle.ready(pr.number, expected_head: @head)
|
|
139
|
+
end
|
|
140
|
+
verify_pr!(receipt.pull_request)
|
|
141
|
+
unless receipt.operation.to_s == operation && receipt.server_name == server["name"] &&
|
|
142
|
+
(operation != "create" || receipt.pull_request.draft == true) &&
|
|
143
|
+
(operation != "ready" || receipt.pull_request.draft == false)
|
|
144
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "Delivery mutation lacks expected outcome"
|
|
145
|
+
end
|
|
146
|
+
record_result(operation, receipt.pull_request, "succeeded")
|
|
147
|
+
status
|
|
148
|
+
rescue Ace::Git::ProviderUnknownOutcomeError
|
|
149
|
+
# The durable intent remains unresolved; no exception text or
|
|
150
|
+
# provider payload is stored (both may contain sensitive data).
|
|
151
|
+
raise
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def events
|
|
155
|
+
@journal.read_events(@binding.fetch("assignment_id")).select do |event|
|
|
156
|
+
event["type"] == "delivery"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def record(payload)
|
|
161
|
+
producer = {"actor" => @identity.actor, "role" => @identity.role, "runtime" => @identity.runtime}
|
|
162
|
+
@journal.record(**@binding.transform_keys(&:to_sym), type: "delivery", payload: payload.merge("producer" => producer),
|
|
163
|
+
guard: -> {
|
|
164
|
+
live = @coordinator.authoritative_attempt(@binding)
|
|
165
|
+
raise AttemptErrors::ReceiptRejected, "Delivery attempt is no longer active" unless live&.active?
|
|
166
|
+
raise AttemptErrors::ReceiptRejected, "Candidate changed during delivery" unless git!("rev-parse", "HEAD").strip == @head
|
|
167
|
+
})
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def unresolved_intent
|
|
171
|
+
history = events
|
|
172
|
+
settled = history.filter_map do |event|
|
|
173
|
+
payload = event.fetch("payload")
|
|
174
|
+
payload["intent_digest"] if payload["stage"] == "result" && payload["outcome"] == "succeeded"
|
|
175
|
+
end
|
|
176
|
+
pending = history.select do |event|
|
|
177
|
+
event.dig("payload", "stage") == "intent" && !settled.include?(event["digest"])
|
|
178
|
+
end
|
|
179
|
+
raise AttemptErrors::Conflict, "Multiple unresolved delivery intents require reconciliation" if pending.length > 1
|
|
180
|
+
event = pending.first
|
|
181
|
+
event && event.fetch("payload").merge("intent_digest" => event.fetch("digest"),
|
|
182
|
+
"intent_attempt_id" => event.fetch("attempt_id"))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def current_pr
|
|
186
|
+
result = events.reverse.find { |event| event.dig("payload", "stage") == "result" && event.dig("payload", "pr") }
|
|
187
|
+
result && @lifecycle.show(result.dig("payload", "pr", "number"))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def reconcile(intent)
|
|
191
|
+
@intent_context = intent
|
|
192
|
+
raise AttemptErrors::ReceiptRejected, "Unresolved delivery belongs to an older candidate" unless intent["head"] == @head
|
|
193
|
+
operation = intent.fetch("operation")
|
|
194
|
+
if operation == "create"
|
|
195
|
+
matches = @lifecycle.reconcile_create(**@provenance.reject { |key, _| key == "mode" }.transform_keys(&:to_sym))
|
|
196
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "No exact draft found; create remains unresolved" if matches.empty?
|
|
197
|
+
raise Ace::Git::ProviderConflictingMatchesError, "Multiple exact drafts found" unless matches.length == 1
|
|
198
|
+
pr = matches.first
|
|
199
|
+
verify_pr!(pr)
|
|
200
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "Created PR draft state is unproven" unless pr.draft == true
|
|
201
|
+
pr = @lifecycle.review_metadata_snapshot(pr.number).pull_request
|
|
202
|
+
verify_pr!(pr)
|
|
203
|
+
unless content_matches?(intent.fetch("fields"), intent.fetch("input_digest"), pr)
|
|
204
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "Created PR content remains unproven"
|
|
205
|
+
end
|
|
206
|
+
elsif operation == "ready"
|
|
207
|
+
require_evidence!(intent["tests"], intent["review"])
|
|
208
|
+
pr = @lifecycle.show(intent.fetch("pr_number"))
|
|
209
|
+
verify_pr!(pr)
|
|
210
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "Readiness remains unresolved" unless pr.draft == false
|
|
211
|
+
elsif operation == "update"
|
|
212
|
+
pr = @lifecycle.review_metadata_snapshot(intent.fetch("pr_number")).pull_request
|
|
213
|
+
verify_pr!(pr)
|
|
214
|
+
unless content_matches?(intent.fetch("fields"), intent.fetch("input_digest"), pr)
|
|
215
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "PR update remains unresolved"
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
raise Ace::Git::ProviderUnknownOutcomeError, "#{operation} requires authoritative receipt reconciliation"
|
|
219
|
+
end
|
|
220
|
+
record_result(operation, pr, "succeeded")
|
|
221
|
+
status
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def content_input(options)
|
|
225
|
+
options.slice(:title, :body).compact
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def content_matches?(fields, digest, pr)
|
|
229
|
+
observed = fields.to_h { |field| [field, pr.public_send(field)] }
|
|
230
|
+
Atoms::EvidenceDigest.digest(observed) == digest
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def same_effect?(intent, operation, options)
|
|
234
|
+
return false unless intent["operation"] == operation
|
|
235
|
+
case operation
|
|
236
|
+
when "create", "update"
|
|
237
|
+
intent["input_digest"] == Atoms::EvidenceDigest.digest(content_input(options))
|
|
238
|
+
when "ready"
|
|
239
|
+
%i[tests review].all? { |key| options[key].nil? || options[key] == intent[key.to_s] }
|
|
240
|
+
when "merge"
|
|
241
|
+
options[:service_request_id].is_a?(String) &&
|
|
242
|
+
options[:service_request_id] == intent["service_request_id"] &&
|
|
243
|
+
%i[tests review].all? { |key| options[key].nil? || options[key] == intent[key.to_s] }
|
|
244
|
+
else false
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def require_evidence!(tests, review)
|
|
249
|
+
[[tests, "check"], [review, "review-approval"]].each do |ref, kind|
|
|
250
|
+
unless ref.is_a?(Hash) && ref.keys.sort == %w[attempt_id receipt_digest]
|
|
251
|
+
raise AttemptErrors::ReceiptRejected, "Current accepted #{kind} evidence is required"
|
|
252
|
+
end
|
|
253
|
+
evidence = @coordinator.evidence(attempt_id: ref["attempt_id"], receipt_digest: ref["receipt_digest"], kind: kind)
|
|
254
|
+
unless evidence["head"] == @head && evidence["assignment_id"] == @binding["assignment_id"] &&
|
|
255
|
+
evidence["project_id"] == @attempt.binding.project_id
|
|
256
|
+
raise AttemptErrors::ReceiptRejected, "Delivery evidence does not match assignment, project and candidate"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def consume_merge(request_id)
|
|
262
|
+
unless request_id.is_a?(String) && request_id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/)
|
|
263
|
+
raise AttemptErrors::ReceiptRejected, "Exact authorized merge service receipt is required"
|
|
264
|
+
end
|
|
265
|
+
request = @coordinator.service_request_status(request_id)
|
|
266
|
+
unless request && request["state"] == "succeeded" && request["operation"] == "merge" &&
|
|
267
|
+
request["assignment_id"] == @binding["assignment_id"] && request["attempt_id"] == @binding["attempt_id"] &&
|
|
268
|
+
request["project_id"] == @attempt.binding.project_id && request["candidate_head"] == @head &&
|
|
269
|
+
request["authorization"].is_a?(String) && !request["authorization"].empty?
|
|
270
|
+
raise AttemptErrors::ReceiptRejected, "Exact authorized merge service receipt is required"
|
|
271
|
+
end
|
|
272
|
+
@coordinator.validate_service_receipt!(request, "succeeded", request["receipt"])
|
|
273
|
+
pr = current_pr
|
|
274
|
+
verify_pr!(pr)
|
|
275
|
+
unless request.dig("target", "resource") == pr.url && pr.state.to_s == "merged" &&
|
|
276
|
+
pr.merge_commit_sha.to_s.match?(/\A[0-9a-f]{40}\z/)
|
|
277
|
+
raise AttemptErrors::ReceiptRejected, "Service receipt and merged PR identity do not agree"
|
|
278
|
+
end
|
|
279
|
+
record_result("merge", pr, "succeeded", "service_request_id" => request_id)
|
|
280
|
+
status
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def review_snapshot
|
|
284
|
+
pr = current_pr
|
|
285
|
+
raise AttemptErrors::ReceiptRejected, "Create a draft before remote review" unless pr
|
|
286
|
+
verify_pr!(pr)
|
|
287
|
+
snapshot = @lifecycle.review_snapshot(pr.number)
|
|
288
|
+
verify_pr!(snapshot.pull_request)
|
|
289
|
+
snapshot
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def verify_pr!(pr)
|
|
293
|
+
reference = pr && Ace::Git::Atoms::PrReference.parse(pr.url)
|
|
294
|
+
unless reference && reference.number == pr.number && reference.repository_url &&
|
|
295
|
+
Ace::Git::Atoms::ServerUrl.match?(reference.repository_url, @provenance["base_repository_url"])
|
|
296
|
+
raise Ace::Git::ProviderIdentityMismatchError, "PR URL does not match delivery repository and number"
|
|
297
|
+
end
|
|
298
|
+
Atoms::DeliveryParameters.validate_url!(pr.url)
|
|
299
|
+
unless pr && pr.server_name == @server["name"] && pr.number.is_a?(Integer) && pr.number.positive? &&
|
|
300
|
+
pr.head_sha == @head && pr.head_ref == @provenance["head_ref"] &&
|
|
301
|
+
pr.base_ref == @provenance["base_ref"] &&
|
|
302
|
+
Ace::Git::Atoms::ServerUrl.match?(pr.head_repository_url, @provenance["head_repository_url"]) &&
|
|
303
|
+
Ace::Git::Atoms::ServerUrl.match?(pr.base_repository_url, @provenance["base_repository_url"])
|
|
304
|
+
raise Ace::Git::ProviderExpectedHeadConflictError, "PR candidate or provenance does not match delivery"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def verify_pushed_head!
|
|
309
|
+
output = git!("ls-remote", "--heads", @provenance["head_repository_url"], "refs/heads/#{@provenance['head_ref']}")
|
|
310
|
+
expected = "#{@head}\trefs/heads/#{@provenance['head_ref']}"
|
|
311
|
+
raise AttemptErrors::ReceiptRejected, "Pushed source does not match candidate" unless output.strip == expected
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def record_result(operation, pr, outcome, extra = {})
|
|
315
|
+
record({"stage" => "result", "operation" => operation, "head" => @head, "outcome" => outcome,
|
|
316
|
+
"intent_digest" => @intent_context&.fetch("intent_digest", nil),
|
|
317
|
+
"intent_attempt_id" => @intent_context&.fetch("intent_attempt_id", nil),
|
|
318
|
+
"pr" => {"number" => pr.number, "url" => pr.url, "head_sha" => pr.head_sha,
|
|
319
|
+
"draft" => pr.draft, "state" => pr.state.to_s, "merge_commit_sha" => pr.merge_commit_sha}}.merge(extra))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def status
|
|
323
|
+
attempt = @coordinator.authoritative_attempt(@binding)
|
|
324
|
+
{"attempt_id" => attempt.attempt_id, "assignment_id" => @binding["assignment_id"],
|
|
325
|
+
"task_id" => attempt.binding.task_id, "project_id" => attempt.binding.project_id,
|
|
326
|
+
"actor" => attempt.binding.actor, "role" => attempt.binding.role, "runtime" => attempt.binding.runtime,
|
|
327
|
+
"base_head" => attempt.binding.base_head, "candidate_head" => attempt.candidate_head,
|
|
328
|
+
"evidence_git_ref" => @attempt.binding.evidence_git_ref, "journal_commit" => @journal.ref_value,
|
|
329
|
+
"delivery" => events.map do |event|
|
|
330
|
+
event.fetch("payload").merge("attempt_id" => event.fetch("attempt_id"), "event_digest" => event.fetch("digest"))
|
|
331
|
+
end}
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def git!(*argv)
|
|
335
|
+
result = Ace::Git::Atoms::CommandExecutor.execute("git", "-C", @repo_root, *argv)
|
|
336
|
+
raise AttemptErrors::EvidenceUnavailable, "Git delivery proof unavailable" unless result[:success]
|
|
337
|
+
result[:output]
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
@@ -21,7 +21,7 @@ module Ace
|
|
|
21
21
|
@executor = executor || AssignmentExecutor.new
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def call(task_refs:, preset_name: DEFAULT_PRESET, primary_task_ref: nil)
|
|
24
|
+
def call(task_refs:, preset_name: DEFAULT_PRESET, primary_task_ref: nil, delivery_parameters: nil)
|
|
25
25
|
requested_refs = normalize_requested_refs(task_refs)
|
|
26
26
|
raise Ace::Support::Cli::Error, "--task requires at least one task reference" if requested_refs.empty?
|
|
27
27
|
|
|
@@ -48,10 +48,14 @@ module Ace
|
|
|
48
48
|
validate_parameters!(preset, params)
|
|
49
49
|
|
|
50
50
|
steps = Atoms::PresetExpander.expand(preset, params)
|
|
51
|
+
delivery = delivery_parameters && Atoms::DeliveryParameters.validate(delivery_parameters)
|
|
52
|
+
if delivery && !steps.any? { |step| %w[create-pr update-pr-desc mark-pr-ready].include?(step["name"]) }
|
|
53
|
+
raise ArgumentError, "Explicit delivery inputs require a remote-capable preset"
|
|
54
|
+
end
|
|
51
55
|
job_path = write_job_file(
|
|
52
56
|
session_name: "#{preset_name}-#{primary_ref}",
|
|
53
57
|
description: preset["description"],
|
|
54
|
-
steps: steps
|
|
58
|
+
steps: steps, delivery: delivery
|
|
55
59
|
)
|
|
56
60
|
|
|
57
61
|
result = @executor.start(job_path, task_id: primary_ref)
|
|
@@ -147,7 +151,7 @@ module Ace
|
|
|
147
151
|
raise Ace::Support::Cli::Error, errors.join(", ")
|
|
148
152
|
end
|
|
149
153
|
|
|
150
|
-
def write_job_file(session_name:, description:, steps:)
|
|
154
|
+
def write_job_file(session_name:, description:, steps:, delivery: nil)
|
|
151
155
|
job = {
|
|
152
156
|
"session" => {
|
|
153
157
|
"name" => session_name,
|
|
@@ -156,6 +160,7 @@ module Ace
|
|
|
156
160
|
"steps" => steps
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
job["delivery"] = delivery if delivery
|
|
159
164
|
dir = File.join(Ace::Assign.cache_dir, "jobs")
|
|
160
165
|
FileUtils.mkdir_p(dir)
|
|
161
166
|
path = File.join(dir, "#{session_name}-#{SecureRandom.hex(4)}-job.yml")
|
data/lib/ace/assign/version.rb
CHANGED
data/lib/ace/assign.rb
CHANGED
|
@@ -101,6 +101,16 @@ module Ace
|
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Byte framing failed before any business receipt could be admitted.
|
|
105
|
+
class MalformedTransfer < ReceiptRejected; end
|
|
106
|
+
|
|
107
|
+
# Recovery settled a prior effect, but did not perform this request.
|
|
108
|
+
class CurrentEffectRequired < ReceiptRejected
|
|
109
|
+
def initialize(operation:, recovered_operation:)
|
|
110
|
+
super("Recovered #{recovered_operation}; requested #{operation} still needs execution")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
104
114
|
# Attempt state forbids the requested effect (terminal, uncertain, blocked)
|
|
105
115
|
class InvalidState < Error
|
|
106
116
|
def initialize(message = "Attempt state forbids the requested operation")
|
|
@@ -197,3 +207,6 @@ module Ace
|
|
|
197
207
|
private_class_method :load_gem_defaults_fallback
|
|
198
208
|
end
|
|
199
209
|
end
|
|
210
|
+
|
|
211
|
+
require_relative "assign/atoms/delivery_parameters"
|
|
212
|
+
require_relative "assign/organisms/delivery_coordinator"
|
metadata
CHANGED
|
@@ -1,14 +1,76 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-assign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.64.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ace-llm-providers-cli
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.36.1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.36.1
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ace-support-fs
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ace-git
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.29'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.29'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: json
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.20'
|
|
61
|
+
- - "<"
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '3'
|
|
64
|
+
type: :runtime
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '2.20'
|
|
71
|
+
- - "<"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3'
|
|
12
74
|
- !ruby/object:Gem::Dependency
|
|
13
75
|
name: ace-support-cli
|
|
14
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -113,14 +175,14 @@ dependencies:
|
|
|
113
175
|
requirements:
|
|
114
176
|
- - "~>"
|
|
115
177
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: '0.
|
|
178
|
+
version: '0.59'
|
|
117
179
|
type: :runtime
|
|
118
180
|
prerelease: false
|
|
119
181
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
182
|
requirements:
|
|
121
183
|
- - "~>"
|
|
122
184
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0.
|
|
185
|
+
version: '0.59'
|
|
124
186
|
- !ruby/object:Gem::Dependency
|
|
125
187
|
name: ace-task
|
|
126
188
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -141,14 +203,14 @@ dependencies:
|
|
|
141
203
|
requirements:
|
|
142
204
|
- - "~>"
|
|
143
205
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: '0.
|
|
206
|
+
version: '0.2'
|
|
145
207
|
type: :runtime
|
|
146
208
|
prerelease: false
|
|
147
209
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
210
|
requirements:
|
|
149
211
|
- - "~>"
|
|
150
212
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: '0.
|
|
213
|
+
version: '0.2'
|
|
152
214
|
- !ruby/object:Gem::Dependency
|
|
153
215
|
name: ace-tmux
|
|
154
216
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,6 +225,20 @@ dependencies:
|
|
|
163
225
|
- - "~>"
|
|
164
226
|
- !ruby/object:Gem::Version
|
|
165
227
|
version: '0.18'
|
|
228
|
+
- !ruby/object:Gem::Dependency
|
|
229
|
+
name: ace-herdr
|
|
230
|
+
requirement: !ruby/object:Gem::Requirement
|
|
231
|
+
requirements:
|
|
232
|
+
- - "~>"
|
|
233
|
+
- !ruby/object:Gem::Version
|
|
234
|
+
version: '0.4'
|
|
235
|
+
type: :runtime
|
|
236
|
+
prerelease: false
|
|
237
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
238
|
+
requirements:
|
|
239
|
+
- - "~>"
|
|
240
|
+
- !ruby/object:Gem::Version
|
|
241
|
+
version: '0.4'
|
|
166
242
|
- !ruby/object:Gem::Dependency
|
|
167
243
|
name: ace-support-test-helpers
|
|
168
244
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -275,6 +351,7 @@ files:
|
|
|
275
351
|
- docs/exit-codes.md
|
|
276
352
|
- docs/getting-started.md
|
|
277
353
|
- docs/handbook.md
|
|
354
|
+
- docs/protected-authority.md
|
|
278
355
|
- docs/usage.md
|
|
279
356
|
- exe/ace-assign
|
|
280
357
|
- handbook/guides/fork-context.g.md
|
|
@@ -309,6 +386,7 @@ files:
|
|
|
309
386
|
- lib/ace/assign/atoms/attempt_state_machine.rb
|
|
310
387
|
- lib/ace/assign/atoms/catalog_loader.rb
|
|
311
388
|
- lib/ace/assign/atoms/composition_rules.rb
|
|
389
|
+
- lib/ace/assign/atoms/delivery_parameters.rb
|
|
312
390
|
- lib/ace/assign/atoms/evidence_digest.rb
|
|
313
391
|
- lib/ace/assign/atoms/number_generator.rb
|
|
314
392
|
- lib/ace/assign/atoms/preset_expander.rb
|
|
@@ -318,6 +396,29 @@ files:
|
|
|
318
396
|
- lib/ace/assign/atoms/step_numbering.rb
|
|
319
397
|
- lib/ace/assign/atoms/step_sorter.rb
|
|
320
398
|
- lib/ace/assign/atoms/tree_formatter.rb
|
|
399
|
+
- lib/ace/assign/authority/candidate_transfer.rb
|
|
400
|
+
- lib/ace/assign/authority/client.rb
|
|
401
|
+
- lib/ace/assign/authority/deployment.rb
|
|
402
|
+
- lib/ace/assign/authority/endcap.rb
|
|
403
|
+
- lib/ace/assign/authority/endcap_inboxes.rb
|
|
404
|
+
- lib/ace/assign/authority/endcap_results.rb
|
|
405
|
+
- lib/ace/assign/authority/endcap_services.rb
|
|
406
|
+
- lib/ace/assign/authority/evidence_transfer.rb
|
|
407
|
+
- lib/ace/assign/authority/execution_scope_observation.rb
|
|
408
|
+
- lib/ace/assign/authority/launch_driver.rb
|
|
409
|
+
- lib/ace/assign/authority/launch_lifecycle.rb
|
|
410
|
+
- lib/ace/assign/authority/launch_scope_abort.rb
|
|
411
|
+
- lib/ace/assign/authority/launch_scope_admission.rb
|
|
412
|
+
- lib/ace/assign/authority/launch_scope_close.rb
|
|
413
|
+
- lib/ace/assign/authority/launch_scope_parent.rb
|
|
414
|
+
- lib/ace/assign/authority/launch_scope_release.rb
|
|
415
|
+
- lib/ace/assign/authority/posix_acl.rb
|
|
416
|
+
- lib/ace/assign/authority/private_directory.rb
|
|
417
|
+
- lib/ace/assign/authority/receipt_transfer.rb
|
|
418
|
+
- lib/ace/assign/authority/router.rb
|
|
419
|
+
- lib/ace/assign/authority/server.rb
|
|
420
|
+
- lib/ace/assign/authority/service_evidence.rb
|
|
421
|
+
- lib/ace/assign/authority/transfer_codec.rb
|
|
321
422
|
- lib/ace/assign/cli.rb
|
|
322
423
|
- lib/ace/assign/cli/commands/add.rb
|
|
323
424
|
- lib/ace/assign/cli/commands/assignment_target.rb
|
|
@@ -327,12 +428,19 @@ files:
|
|
|
327
428
|
- lib/ace/assign/cli/commands/attempt/reconcile.rb
|
|
328
429
|
- lib/ace/assign/cli/commands/attempt/start.rb
|
|
329
430
|
- lib/ace/assign/cli/commands/attempt/status.rb
|
|
431
|
+
- lib/ace/assign/cli/commands/authority/launch.rb
|
|
432
|
+
- lib/ace/assign/cli/commands/authority/serve.rb
|
|
433
|
+
- lib/ace/assign/cli/commands/authority/status.rb
|
|
434
|
+
- lib/ace/assign/cli/commands/authority/terminate.rb
|
|
330
435
|
- lib/ace/assign/cli/commands/create.rb
|
|
436
|
+
- lib/ace/assign/cli/commands/delivery.rb
|
|
331
437
|
- lib/ace/assign/cli/commands/fail.rb
|
|
332
438
|
- lib/ace/assign/cli/commands/finish.rb
|
|
333
439
|
- lib/ace/assign/cli/commands/fork_run.rb
|
|
334
440
|
- lib/ace/assign/cli/commands/fork_session.rb
|
|
441
|
+
- lib/ace/assign/cli/commands/inbox_reconcile.rb
|
|
335
442
|
- lib/ace/assign/cli/commands/list.rb
|
|
443
|
+
- lib/ace/assign/cli/commands/resume.rb
|
|
336
444
|
- lib/ace/assign/cli/commands/retry_cmd.rb
|
|
337
445
|
- lib/ace/assign/cli/commands/select.rb
|
|
338
446
|
- lib/ace/assign/cli/commands/start.rb
|
|
@@ -350,12 +458,16 @@ files:
|
|
|
350
458
|
- lib/ace/assign/molecules/assignment_manager.rb
|
|
351
459
|
- lib/ace/assign/molecules/attempt_reconciler.rb
|
|
352
460
|
- lib/ace/assign/molecules/attempt_store.rb
|
|
461
|
+
- lib/ace/assign/molecules/canonical_evidence.rb
|
|
353
462
|
- lib/ace/assign/molecules/evidence_calculator.rb
|
|
354
463
|
- lib/ace/assign/molecules/evidence_journal.rb
|
|
355
464
|
- lib/ace/assign/molecules/execution_identity_resolver.rb
|
|
465
|
+
- lib/ace/assign/molecules/execution_scope_lineage.rb
|
|
356
466
|
- lib/ace/assign/molecules/fork_session_launcher.rb
|
|
467
|
+
- lib/ace/assign/molecules/journal_mutation.rb
|
|
357
468
|
- lib/ace/assign/molecules/lifecycle_exclusion.rb
|
|
358
469
|
- lib/ace/assign/molecules/preset_inferrer.rb
|
|
470
|
+
- lib/ace/assign/molecules/proposal_journal.rb
|
|
359
471
|
- lib/ace/assign/molecules/queue_scanner.rb
|
|
360
472
|
- lib/ace/assign/molecules/receipt_verifier.rb
|
|
361
473
|
- lib/ace/assign/molecules/runtime_control_surface_runner.rb
|
|
@@ -364,6 +476,7 @@ files:
|
|
|
364
476
|
- lib/ace/assign/molecules/step_writer.rb
|
|
365
477
|
- lib/ace/assign/organisms/assignment_executor.rb
|
|
366
478
|
- lib/ace/assign/organisms/attempt_coordinator.rb
|
|
479
|
+
- lib/ace/assign/organisms/delivery_coordinator.rb
|
|
367
480
|
- lib/ace/assign/organisms/task_assignment_creator.rb
|
|
368
481
|
- lib/ace/assign/version.rb
|
|
369
482
|
homepage: https://github.com/cs3b/ace
|