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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  3. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  5. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  7. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  8. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  9. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  10. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  11. data/CHANGELOG.md +51 -0
  12. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  13. data/docs/protected-authority.md +266 -0
  14. data/docs/usage.md +91 -2
  15. data/handbook/workflow-instructions/assign/drive.wf.md +2 -2
  16. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  17. data/lib/ace/assign/atoms/delivery_parameters.rb +54 -0
  18. data/lib/ace/assign/authority/candidate_transfer.rb +160 -0
  19. data/lib/ace/assign/authority/client.rb +107 -0
  20. data/lib/ace/assign/authority/deployment.rb +465 -0
  21. data/lib/ace/assign/authority/endcap.rb +342 -0
  22. data/lib/ace/assign/authority/endcap_inboxes.rb +314 -0
  23. data/lib/ace/assign/authority/endcap_results.rb +380 -0
  24. data/lib/ace/assign/authority/endcap_services.rb +395 -0
  25. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  26. data/lib/ace/assign/authority/execution_scope_observation.rb +476 -0
  27. data/lib/ace/assign/authority/launch_driver.rb +114 -0
  28. data/lib/ace/assign/authority/launch_lifecycle.rb +822 -0
  29. data/lib/ace/assign/authority/launch_scope_abort.rb +88 -0
  30. data/lib/ace/assign/authority/launch_scope_admission.rb +84 -0
  31. data/lib/ace/assign/authority/launch_scope_close.rb +115 -0
  32. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  33. data/lib/ace/assign/authority/launch_scope_release.rb +180 -0
  34. data/lib/ace/assign/authority/posix_acl.rb +56 -0
  35. data/lib/ace/assign/authority/private_directory.rb +33 -0
  36. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  37. data/lib/ace/assign/authority/router.rb +54 -0
  38. data/lib/ace/assign/authority/server.rb +230 -0
  39. data/lib/ace/assign/authority/service_evidence.rb +55 -0
  40. data/lib/ace/assign/authority/transfer_codec.rb +155 -0
  41. data/lib/ace/assign/cli/commands/authority/launch.rb +45 -0
  42. data/lib/ace/assign/cli/commands/authority/serve.rb +30 -0
  43. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  44. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  45. data/lib/ace/assign/cli/commands/create.rb +9 -1
  46. data/lib/ace/assign/cli/commands/delivery.rb +57 -0
  47. data/lib/ace/assign/cli/commands/inbox_reconcile.rb +36 -0
  48. data/lib/ace/assign/cli/commands/resume.rb +26 -0
  49. data/lib/ace/assign/cli.rb +22 -1
  50. data/lib/ace/assign/models/evidence_event.rb +3 -1
  51. data/lib/ace/assign/molecules/attempt_reconciler.rb +31 -12
  52. data/lib/ace/assign/molecules/canonical_evidence.rb +150 -0
  53. data/lib/ace/assign/molecules/evidence_journal.rb +218 -63
  54. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  55. data/lib/ace/assign/molecules/execution_scope_lineage.rb +327 -0
  56. data/lib/ace/assign/molecules/journal_mutation.rb +202 -0
  57. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +4 -0
  58. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  59. data/lib/ace/assign/molecules/receipt_verifier.rb +49 -19
  60. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +4 -1
  61. data/lib/ace/assign/organisms/assignment_executor.rb +6 -0
  62. data/lib/ace/assign/organisms/attempt_coordinator.rb +312 -13
  63. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  64. data/lib/ace/assign/organisms/task_assignment_creator.rb +8 -3
  65. data/lib/ace/assign/version.rb +1 -1
  66. data/lib/ace/assign.rb +13 -0
  67. 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")
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Assign
5
- VERSION = "0.63.0"
5
+ VERSION = "0.64.0"
6
6
  end
7
7
  end
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.63.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-04 00:00:00.000000000 Z
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.57'
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.57'
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.1'
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.1'
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