ace-assign 0.68.0 → 0.70.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/composition-rules.yml +0 -0
  3. data/.ace-defaults/assign/catalog/recipes/batch-tasks.recipe.yml +0 -0
  4. data/.ace-defaults/assign/catalog/recipes/documentation.recipe.yml +0 -0
  5. data/.ace-defaults/assign/catalog/recipes/fix-and-review.recipe.yml +0 -0
  6. data/.ace-defaults/assign/catalog/recipes/implement-simple.recipe.yml +0 -0
  7. data/.ace-defaults/assign/catalog/recipes/implement-with-pr.recipe.yml +0 -0
  8. data/.ace-defaults/assign/catalog/recipes/release-only.recipe.yml +0 -0
  9. data/.ace-defaults/assign/catalog/steps/apply-feedback.step.yml +0 -0
  10. data/.ace-defaults/assign/catalog/steps/commit.step.yml +0 -0
  11. data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +0 -0
  12. data/.ace-defaults/assign/catalog/steps/fix-tests.step.yml +0 -0
  13. data/.ace-defaults/assign/catalog/steps/lint.step.yml +0 -0
  14. data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +0 -0
  15. data/.ace-defaults/assign/catalog/steps/onboard-base.step.yml +0 -0
  16. data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -0
  17. data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -0
  18. data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +0 -0
  19. data/.ace-defaults/assign/catalog/steps/push-to-remote.step.yml +0 -0
  20. data/.ace-defaults/assign/catalog/steps/rebase-with-main.step.yml +0 -0
  21. data/.ace-defaults/assign/catalog/steps/record-demo.step.yml +0 -0
  22. data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +0 -0
  23. data/.ace-defaults/assign/catalog/steps/reorganize-commits.step.yml +0 -0
  24. data/.ace-defaults/assign/catalog/steps/research.step.yml +0 -0
  25. data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -0
  26. data/.ace-defaults/assign/catalog/steps/security-audit.step.yml +0 -0
  27. data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +0 -0
  28. data/.ace-defaults/assign/catalog/steps/squash-changelog.step.yml +0 -0
  29. data/.ace-defaults/assign/catalog/steps/task-load.step.yml +0 -0
  30. data/.ace-defaults/assign/catalog/steps/update-docs.step.yml +0 -0
  31. data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +0 -0
  32. data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +0 -0
  33. data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -0
  34. data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
  35. data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
  36. data/.ace-defaults/assign/presets/quick-implement.yml +0 -0
  37. data/.ace-defaults/assign/presets/release-only.yml +0 -0
  38. data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
  39. data/.ace-defaults/assign/presets/work-on-docs.yml +0 -0
  40. data/.ace-defaults/assign/presets/work-on-task-in-place.yml +0 -0
  41. data/.ace-defaults/nav/protocols/skill-sources/ace-assign.yml +0 -0
  42. data/.ace-defaults/nav/protocols/wfi-sources/ace-assign.yml +0 -0
  43. data/CHANGELOG.md +17 -0
  44. data/Rakefile +0 -0
  45. data/docs/demo/canonical-skill-source.gif +0 -0
  46. data/docs/demo/fork-provider.cast +0 -0
  47. data/docs/demo/fork-provider.gif +0 -0
  48. data/docs/demo/fork-provider.recording.json +0 -0
  49. data/docs/demo/fork-provider.tape.yml +0 -0
  50. data/docs/getting-started.md +0 -0
  51. data/docs/handbook.md +0 -0
  52. data/handbook/guides/fork-context.g.md +0 -0
  53. data/handbook/skills/as-assign-add-task/SKILL.md +0 -0
  54. data/handbook/skills/as-assign-compose/SKILL.md +0 -0
  55. data/handbook/skills/as-assign-create/SKILL.md +0 -0
  56. data/handbook/skills/as-assign-drive/SKILL.md +0 -0
  57. data/handbook/skills/as-assign-prepare/SKILL.md +0 -0
  58. data/handbook/skills/as-assign-recover-fork/SKILL.md +0 -0
  59. data/handbook/skills/as-assign-run-in-batches/SKILL.md +0 -0
  60. data/handbook/skills/as-assign-start/SKILL.md +0 -0
  61. data/handbook/skills/as-create-retro-internal/SKILL.md +0 -0
  62. data/handbook/skills/as-mark-task-done-internal/SKILL.md +0 -0
  63. data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +0 -0
  64. data/handbook/skills/as-task-load-internal/SKILL.md +0 -0
  65. data/handbook/workflow-instructions/assign/add-task.wf.md +0 -0
  66. data/handbook/workflow-instructions/assign/compose.wf.md +0 -0
  67. data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +0 -0
  68. data/handbook/workflow-instructions/assign/create.wf.md +0 -0
  69. data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +0 -0
  70. data/handbook/workflow-instructions/assign/recover-fork.wf.md +0 -0
  71. data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +0 -0
  72. data/handbook/workflow-instructions/assign/run-in-batches.wf.md +0 -0
  73. data/handbook/workflow-instructions/assign/start.wf.md +0 -0
  74. data/handbook/workflow-instructions/assign/task-load-internal.wf.md +0 -0
  75. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +0 -0
  76. data/lib/ace/assign/atoms/assign_frontmatter_parser.rb +0 -0
  77. data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
  78. data/lib/ace/assign/atoms/catalog_loader.rb +0 -0
  79. data/lib/ace/assign/atoms/composition_rules.rb +0 -0
  80. data/lib/ace/assign/atoms/number_generator.rb +0 -0
  81. data/lib/ace/assign/atoms/preset_expander.rb +0 -0
  82. data/lib/ace/assign/atoms/preset_loader.rb +0 -0
  83. data/lib/ace/assign/atoms/preset_step_resolver.rb +0 -0
  84. data/lib/ace/assign/atoms/step_file_parser.rb +0 -0
  85. data/lib/ace/assign/atoms/step_numbering.rb +0 -0
  86. data/lib/ace/assign/atoms/step_sorter.rb +0 -0
  87. data/lib/ace/assign/atoms/tree_formatter.rb +0 -0
  88. data/lib/ace/assign/authority/assignment_definition.rb +35 -0
  89. data/lib/ace/assign/authority/candidate_transfer.rb +2 -2
  90. data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
  91. data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
  92. data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
  93. data/lib/ace/assign/authority/ordinary_client.rb +70 -0
  94. data/lib/ace/assign/authority/ordinary_completion.rb +94 -0
  95. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
  96. data/lib/ace/assign/authority/ordinary_lifecycle.rb +191 -0
  97. data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
  98. data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
  99. data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
  100. data/lib/ace/assign/authority/ordinary_results.rb +196 -0
  101. data/lib/ace/assign/authority/ordinary_server.rb +75 -0
  102. data/lib/ace/assign/authority/prepared_work.rb +2 -2
  103. data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
  104. data/lib/ace/assign/authority/router.rb +11 -6
  105. data/lib/ace/assign/authority/server.rb +11 -180
  106. data/lib/ace/assign/authority/socket_server.rb +255 -0
  107. data/lib/ace/assign/authority/transfer_codec.rb +2 -2
  108. data/lib/ace/assign/cli/commands/add.rb +0 -0
  109. data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
  110. data/lib/ace/assign/cli/commands/list.rb +0 -0
  111. data/lib/ace/assign/cli/commands/retry_cmd.rb +0 -0
  112. data/lib/ace/assign/models/assignment_info.rb +0 -0
  113. data/lib/ace/assign/models/queue_state.rb +0 -0
  114. data/lib/ace/assign/models/step.rb +0 -0
  115. data/lib/ace/assign/molecules/assignment_discoverer.rb +0 -0
  116. data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
  117. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
  118. data/lib/ace/assign/molecules/evidence_journal.rb +141 -34
  119. data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
  120. data/lib/ace/assign/molecules/journal_mutation.rb +5 -3
  121. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
  122. data/lib/ace/assign/molecules/preset_inferrer.rb +0 -0
  123. data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
  124. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +0 -0
  125. data/lib/ace/assign/molecules/step_renumberer.rb +0 -0
  126. data/lib/ace/assign/organisms/attempt_coordinator.rb +58 -36
  127. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
  128. data/lib/ace/assign/version.rb +1 -1
  129. metadata +24 -10
File without changes
@@ -11,7 +11,7 @@ module Ace
11
11
  PROPOSAL_REFERENCE = /\Aproposal-[0-9a-f]{24}-r[1-9][0-9]*\z/
12
12
  PROPOSAL_BINDING = %w[operation project_id assignment_id attempt_id input_digest target candidate_head caller_uid].freeze
13
13
 
14
- PROPOSAL_IMMUTABLE = (PROPOSAL_BINDING + %w[schema proposal_id revision revision_id request_id requester content_digest created_at authorization context options recommendation rationale prerequisites lifecycle_request revision_operation_id expected_source_revision]).freeze
14
+ PROPOSAL_IMMUTABLE = (PROPOSAL_BINDING + %w[schema proposal_id revision revision_id request_id requester content_digest created_at authorization context options recommendation rationale prerequisites lifecycle_request revision_operation_id expected_source_revision silence_delegated]).freeze
15
15
 
16
16
  def proposal_history(id)
17
17
  assignment_ids.flat_map do |assignment|
@@ -110,8 +110,7 @@ module Ace
110
110
  valid &&= updated["revision_operation_id"].nil? && updated["expected_source_revision"].nil?
111
111
  end
112
112
  unresolved = previous && proposal_claims(previous["revision_id"]).any? do |claim|
113
- claim["state"] != "succeeded" &&
114
- !(claim["state"] == "failed-settled" && settlement_evidence_intact?(claim))
113
+ claim["state"] != "succeeded" && !proposal_no_effect?(claim)
115
114
  end
116
115
  raise AttemptErrors::ReceiptRejected, "Claimed proposal cannot be revised; reconcile the existing effect" if unresolved
117
116
  raise AttemptErrors::ReceiptRejected, "Proposal revision must begin with exact immutable delivery scope" unless valid
@@ -119,10 +118,39 @@ module Ace
119
118
  private :validate_proposal_revision!
120
119
 
121
120
  def proposal_claims(reference)
122
- service_request_records.select do |request|
121
+ claims = (service_request_records + fixed_merge_request_records).select do |request|
123
122
  request["authorization"] == reference &&
124
123
  !(request["state"] == "rejected" && request["consumed"] == false)
125
124
  end
125
+ return claims if claims.empty?
126
+ events = read_events(claims.first.fetch("assignment_id"))
127
+ claims.sort_by do |claim|
128
+ kind = claim.key?("queue_entry") ? "merge_admission" : "service_claim"
129
+ events.index do |event|
130
+ event["type"] == kind && event["attempt_id"] == claim.fetch("attempt_id") &&
131
+ event.dig("payload", "request_id") == claim.fetch("request_id")
132
+ end || raise(AttemptErrors::EvidenceUnavailable, "Proposal claim has no canonical admission event")
133
+ end
134
+ end
135
+
136
+ # Request IDs are scoped by record family. A same-text ID in the other
137
+ # family is another effect, never an idempotent replay.
138
+ def proposal_claim_conflict(reference, request_id:, fixed:)
139
+ [[service_request_records, false], [fixed_merge_request_records, true]].each do |records, family|
140
+ conflict = records.find do |claim|
141
+ claim["authorization"] == reference &&
142
+ !(family == fixed && claim["request_id"] == request_id) &&
143
+ !(claim["state"] == "rejected" && claim["consumed"] == false) &&
144
+ !proposal_no_effect?(claim)
145
+ end
146
+ return conflict if conflict
147
+ end
148
+ nil
149
+ end
150
+
151
+ def proposal_no_effect?(claim)
152
+ (claim["state"] == "failed-settled" && settlement_evidence_intact?(claim)) ||
153
+ (claim["state"] == "failed" && claim.key?("queue_entry") && fixed_no_effect_intact?(claim))
126
154
  end
127
155
 
128
156
  def proposal_authorize!(reference, binding)
File without changes
@@ -37,7 +37,10 @@ module Ace
37
37
  # @param identity_resolver [Molecules::ExecutionIdentityResolver, nil] Trust boundary
38
38
  # @param verifier [Molecules::ReceiptVerifier, nil] Receipt verifier
39
39
  def initialize(cache_base: nil, repo_root: nil, journal: nil, identity_resolver: nil, verifier: nil,
40
- lifecycle_exclusion: nil)
40
+ lifecycle_exclusion: nil, recorded_binding_observer: nil)
41
+ unless recorded_binding_observer.nil? || recorded_binding_observer.respond_to?(:call)
42
+ raise ArgumentError, "recorded native binding observer must be callable"
43
+ end
41
44
  @manager = Molecules::AssignmentManager.new(cache_base: cache_base)
42
45
  @store = @manager.attempt_store
43
46
  @repo_root = repo_root || Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
@@ -45,6 +48,7 @@ module Ace
45
48
  @identity_resolver = identity_resolver || Molecules::ExecutionIdentityResolver.new
46
49
  @verifier = verifier || Molecules::ReceiptVerifier.new(identity_resolver: @identity_resolver)
47
50
  @lifecycle_exclusion = lifecycle_exclusion
51
+ @recorded_binding_observer = recorded_binding_observer
48
52
  end
49
53
 
50
54
  attr_reader :store
@@ -59,16 +63,17 @@ module Ace
59
63
  {events: [{type: "attempt_stopped", payload: payload}], blobs: {}}
60
64
  end
61
65
 
62
- # The protected result owner authenticates imported bytes, candidate,
66
+ # The source-selected result owner authenticates imported bytes, candidate,
63
67
  # closure and independent settlements at the same CAS prefix.
64
- def self.finished_transition_plan(events:, receipt:)
68
+ def self.finished_transition_plan(events:, receipt:, authority:)
69
+ raise ArgumentError, "unknown result authority" unless %i[ordinary protected].include?(authority)
65
70
  state = Molecules::CanonicalAttemptState.derive(events)
66
71
  unless state == "running" && %w[succeeded failed].include?(receipt.fetch("verdict"))
67
72
  raise AttemptErrors::Conflict, "Result finish requires a running attempt"
68
73
  end
69
74
  Atoms::AttemptStateMachine.transition!(state, receipt.fetch("verdict"))
70
75
  {events: [{type: "receipt_accepted", payload: {"receipt" => receipt}},
71
- {type: "transition", payload: {"from" => state, "to" => receipt.fetch("verdict"), "reason" => "protected_result_accepted"}}], blobs: {}}
76
+ {type: "transition", payload: {"from" => state, "to" => receipt.fetch("verdict"), "reason" => "#{authority}_result_accepted"}}], blobs: {}}
72
77
  end
73
78
 
74
79
  def self.protected_recovery_decision(state:, effects_pending:, bound:, live:, checkpoint:)
@@ -113,21 +118,13 @@ module Ace
113
118
  raise AttemptErrors::ReceiptRejected, "Native reverse requires an active managed attempt"
114
119
  end
115
120
  binding = reconciler.recorded_binding(attempt)
116
- observation = reconciler.observation(attempt)
117
- unless binding && observation["liveness"] == "live" &&
118
- binding["process_identity"] == observation["identity"] &&
119
- %w[runtime session pane].all? { |key| binding[key].is_a?(String) && !binding[key].empty? }
121
+ unless binding && %w[runtime session pane].all? { |key| binding[key].is_a?(String) && !binding[key].empty? }
120
122
  raise AttemptErrors::ReceiptRejected, "Exact native owner binding is unavailable or changed"
121
123
  end
122
- unless caller_pid.nil?
123
- unless caller_pid.is_a?(Integer) && caller_pid.positive?
124
- raise AttemptErrors::UnauthorizedIdentity, "Kernel caller process is unavailable"
125
- end
126
- caller = Ace::Runtime.resolve(binding["runtime"]).process_binding(pane: binding["pane"], caller_pid: caller_pid)
127
- unless caller == binding
128
- raise AttemptErrors::UnauthorizedIdentity, "Caller does not descend from the recorded native owner"
129
- end
124
+ unless caller_pid.nil? || (caller_pid.is_a?(Integer) && caller_pid.positive?)
125
+ raise AttemptErrors::UnauthorizedIdentity, "Kernel caller process is unavailable"
130
126
  end
127
+ observe_runtime_binding!(attempt, binding, caller_pid)
131
128
  latest = recover_managed_attempt(attempt.binding.assignment_id, attempt_id)
132
129
  unless latest&.active? && latest.binding.to_h == attempt.binding.to_h && reconciler.recorded_binding(latest) == binding
133
130
  raise AttemptErrors::ReceiptRejected, "Native owner changed during observation"
@@ -137,6 +134,28 @@ module Ace
137
134
  raise AttemptErrors::ReceiptRejected, "Native owner observation unavailable (#{e.class})"
138
135
  end
139
136
 
137
+ # An installed consumer selects its own bounded native reader. It must
138
+ # never fall back to the service process's ambient runtime on failure.
139
+ def observe_runtime_binding!(attempt, binding, caller_pid)
140
+ if @recorded_binding_observer
141
+ actual = @recorded_binding_observer.call(binding: binding, caller_pid: caller_pid)
142
+ unless actual == binding
143
+ raise AttemptErrors::UnauthorizedIdentity, "Installed native owner observation differs"
144
+ end
145
+ return
146
+ end
147
+ observation = reconciler.observation(attempt)
148
+ unless observation["liveness"] == "live" && binding["process_identity"] == observation["identity"]
149
+ raise AttemptErrors::ReceiptRejected, "Exact native owner binding is unavailable or changed"
150
+ end
151
+ return if caller_pid.nil?
152
+ caller = Ace::Runtime.resolve(binding["runtime"]).process_binding(pane: binding["pane"], caller_pid: caller_pid)
153
+ unless caller == binding
154
+ raise AttemptErrors::UnauthorizedIdentity, "Caller does not descend from the recorded native owner"
155
+ end
156
+ end
157
+ private :observe_runtime_binding!
158
+
140
159
  # Claim an external service effect before dispatch. Only a managed,
141
160
  # active attempt for the exact project and candidate may own it. The
142
161
  # guard re-runs inside the journal lock per CAS attempt, so a
@@ -174,7 +193,10 @@ module Ace
174
193
  end
175
194
 
176
195
  def service_attempt(binding)
177
- attempt = authoritative_attempt(binding)
196
+ # An effect can never acquire authority from a requester cache, even
197
+ # when the assignment definition is missing from this consumer.
198
+ attempt = journal_for.derived_attempts(binding.fetch("assignment_id"))
199
+ .find { |candidate| candidate.attempt_id == binding.fetch("attempt_id") }
178
200
  raise AttemptErrors::NotFound, "Attempt not found" unless attempt
179
201
  unless attempt.managed? && attempt.active? &&
180
202
  attempt.binding.assignment_id == binding["assignment_id"] &&
@@ -306,28 +328,29 @@ module Ace
306
328
  # never need message-string discrimination: replay/binding conflicts
307
329
  # stay Conflict, a consumed authorization is AuthorizationConflict.
308
330
  guard = lambda do
309
- conflict = @journal.fixed_authorization_conflict(record)
331
+ conflict = journal_for.fixed_authorization_conflict(record)
310
332
  if conflict
311
333
  raise AttemptErrors::AuthorizationConflict,
312
334
  "Authorization reference is not currently valid"
313
335
  end
314
336
  nil
315
337
  end
316
- @journal.admit_fixed_merge_request(record, guard: guard)
338
+ journal_for.admit_fixed_merge_request(record, guard: guard)
317
339
  end
318
340
 
319
341
  # Begin the fixed effect (pending -> uncertain). The single CAS
320
342
  # transition is the at-most-one-merge proof: a restarted receiver
321
- # replays "already started" and must not dispatch again.
343
+ # refuses an already-started claim and must not dispatch again.
322
344
  def begin_fixed_merge_effect(request_id)
323
- @journal.begin_fixed_merge_request(request_id)
345
+ journal_for.begin_fixed_merge_request(request_id)
324
346
  end
325
347
 
326
348
  # Settle the fixed effect with the exact terminal receipt. The
327
349
  # journal validates the closed receipt schema and its binding to the
328
350
  # admitted record before the transition lands.
329
- def settle_fixed_merge_effect(request_id, terminal_status:, receipt:)
330
- @journal.settle_fixed_merge_request(request_id, terminal_status: terminal_status, receipt: receipt)
351
+ def settle_fixed_merge_effect(request_id, terminal_status:, receipt:, attestation: nil)
352
+ journal_for.settle_fixed_merge_request(request_id, terminal_status: terminal_status,
353
+ receipt: receipt, attestation: attestation)
331
354
  end
332
355
 
333
356
  # Read-only canonical record lookup by request ID: lets the producer
@@ -337,6 +360,13 @@ module Ace
337
360
  journal_for.fixed_merge_request(request_id)
338
361
  end
339
362
 
363
+ # Read-only listing of every canonical fixed merge record: lets the
364
+ # receiver restart reconciliation find its own still-pending records
365
+ # by binding. Read-only; performs no dispatch and owns no state.
366
+ def fixed_merge_records
367
+ journal_for.fixed_merge_request_records
368
+ end
369
+
340
370
  # Read-only status: resolve the opaque reference against the
341
371
  # canonical record and verify every selector. Performs no dispatch,
342
372
  # retry, authorization refresh or selector repair. Returns the
@@ -345,7 +375,7 @@ module Ace
345
375
  # without revealing whether another project holds the reference.
346
376
  def fixed_merge_status(reference:, request_id:, project_id:, assignment_id:, attempt_id:,
347
377
  queue_entry:, input_digest:)
348
- record = @journal.fixed_merge_request(request_id)
378
+ record = journal_for.fixed_merge_request(request_id)
349
379
  return nil unless record
350
380
  matches = reference == record["reference"] && input_digest == record["input_digest"] &&
351
381
  [project_id, assignment_id, attempt_id, queue_entry].zip(
@@ -596,7 +626,7 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
596
626
 
597
627
  active_scopes = @store.list(assignment_id).select(&:active?).map(&:binding)
598
628
  active_scopes.concat(journal_actives.map(&:binding))
599
- blocker = active_scopes.find { |binding| scopes_overlap?(binding.scope, scope) }
629
+ blocker = active_scopes.find { |binding| Atoms::AssignmentScope.overlap?(binding.scope, scope) }
600
630
  if blocker
601
631
  raise AttemptErrors::Conflict,
602
632
  "Active attempt scope #{blocker.scope} overlaps requested scope #{scope} on #{assignment_id}; " \
@@ -835,9 +865,9 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
835
865
  unless %w[check review-collection review-approval].include?(kind) && check_name.is_a?(String) && !check_name.strip.empty?
836
866
  raise AttemptErrors::ReceiptRejected, "Invalid evidence kind or check name"
837
867
  end
838
- if !historical_head.nil? && (!%w[review-collection review-approval].include?(kind) || !historical_head.is_a?(String) ||
868
+ if !historical_head.nil? && (!historical_head.is_a?(String) ||
839
869
  !historical_head.match?(/\A[0-9a-f]{40,64}\z/))
840
- raise AttemptErrors::ReceiptRejected, "Historical evidence requires review collection/approval and an exact recorded head"
870
+ raise AttemptErrors::ReceiptRejected, "Historical evidence requires an exact recorded head"
841
871
  end
842
872
  unless attempt_id.to_s.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/) &&
843
873
  receipt_digest.to_s.match?(/\A[0-9a-f]{64}\z/)
@@ -987,14 +1017,6 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
987
1017
  raise Error, "Failed to generate a unique attempt ID for #{assignment_id} after 100 attempts"
988
1018
  end
989
1019
 
990
- # Ancestors and descendants overlap: an active attempt for 010 owns
991
- # its 010.01 subtree, and vice versa.
992
- def scopes_overlap?(left, right)
993
- return true if Atoms::AssignmentScope.equal?(left, right)
994
-
995
- left.start_with?("#{right}.") || right.start_with?("#{left}.")
996
- end
997
-
998
1020
  # Active attempts derived from the authoritative journal, used when
999
1021
  # the local record may be missing (lost or wiped cache). Derivation
1000
1022
  # lives in the journal (owner of attempt-state interpretation).
@@ -1052,7 +1074,7 @@ input:#{Regexp.escape(request.fetch("input_digest"))} outcome:(\S+)( no-effect:(
1052
1074
  # merges both). The loser records a stopped transition and refuses.
1053
1075
  def yield_lost_ownership_race(attempt, assignment_id, scope)
1054
1076
  rival = journal_for.active_attempts(assignment_id).find do |other|
1055
- other.attempt_id != attempt.attempt_id && scopes_overlap?(other.binding.scope, scope)
1077
+ other.attempt_id != attempt.attempt_id && Atoms::AssignmentScope.overlap?(other.binding.scope, scope)
1056
1078
  end
1057
1079
  return attempt if rival.nil?
1058
1080
 
@@ -212,7 +212,7 @@ module Ace
212
212
  return {state: "accepted", reference: ack.fetch("reference"),
213
213
  request_id: ack.fetch("request_id"), input_digest: ack.fetch("input_digest")}
214
214
  end
215
- handle_refusal(binding, identity, branch[:payload], expected_digest: digest)
215
+ handle_refusal(binding, identity, branch[:payload], expected_digest: digest, request_id: selectors.fetch(:request_id))
216
216
  end
217
217
 
218
218
  # Closed outer envelope for admission responses: exactly {status,
@@ -241,7 +241,7 @@ module Ace
241
241
  end
242
242
  end
243
243
 
244
- def handle_refusal(binding, identity, error, expected_digest:)
244
+ def handle_refusal(binding, identity, error, expected_digest:, request_id:)
245
245
  refusal = begin
246
246
  Molecules::OrdinaryMergeContract.validate_refusal!(error)
247
247
  rescue ArgumentError
@@ -249,9 +249,9 @@ module Ace
249
249
  "ordinary merge refusal is not the maintained contract; admission stays unconfirmed"
250
250
  end
251
251
  echo = refusal.fetch("input_digest")
252
- if echo && echo != expected_digest
252
+ if refusal.fetch("request_id") != request_id || (echo && echo != expected_digest)
253
253
  raise AttemptErrors::EvidenceUnavailable,
254
- "ordinary merge refusal echoes a different input digest; admission stays unconfirmed"
254
+ "ordinary merge refusal echoes a different request; admission stays unconfirmed"
255
255
  end
256
256
  if refusal.fetch("code") == Molecules::OrdinaryMergeContract::UNAVAILABLE_CODE
257
257
  raise AttemptErrors::EvidenceUnavailable,
@@ -259,7 +259,7 @@ module Ace
259
259
  end
260
260
  record_event(binding, identity, {"stage" => RESULT_STAGE, "outcome" => "refused_no_effect",
261
261
  "code" => refusal.fetch("code"), "message" => refusal.fetch("message"),
262
- "request_id" => refusal.fetch("request_id"), "input_digest" => refusal.fetch("input_digest")})
262
+ "request_id" => request_id, "input_digest" => expected_digest})
263
263
  {state: "refused", code: refusal.fetch("code"), message: refusal.fetch("message")}
264
264
  end
265
265
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Assign
5
- VERSION = "0.68.0"
5
+ VERSION = "0.70.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-assign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.68.0
4
+ version: 0.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-14 00:00:00.000000000 Z
10
+ date: 2026-10-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-llm-providers-cli
@@ -175,42 +175,42 @@ dependencies:
175
175
  requirements:
176
176
  - - "~>"
177
177
  - !ruby/object:Gem::Version
178
- version: 0.60.1
178
+ version: 0.60.3
179
179
  type: :runtime
180
180
  prerelease: false
181
181
  version_requirements: !ruby/object:Gem::Requirement
182
182
  requirements:
183
183
  - - "~>"
184
184
  - !ruby/object:Gem::Version
185
- version: 0.60.1
185
+ version: 0.60.3
186
186
  - !ruby/object:Gem::Dependency
187
187
  name: ace-task
188
188
  requirement: !ruby/object:Gem::Requirement
189
189
  requirements:
190
190
  - - "~>"
191
191
  - !ruby/object:Gem::Version
192
- version: '0.37'
192
+ version: '0.40'
193
193
  type: :runtime
194
194
  prerelease: false
195
195
  version_requirements: !ruby/object:Gem::Requirement
196
196
  requirements:
197
197
  - - "~>"
198
198
  - !ruby/object:Gem::Version
199
- version: '0.37'
199
+ version: '0.40'
200
200
  - !ruby/object:Gem::Dependency
201
201
  name: ace-bundle
202
202
  requirement: !ruby/object:Gem::Requirement
203
203
  requirements:
204
204
  - - "~>"
205
205
  - !ruby/object:Gem::Version
206
- version: '0.45'
206
+ version: 0.45.1
207
207
  type: :runtime
208
208
  prerelease: false
209
209
  version_requirements: !ruby/object:Gem::Requirement
210
210
  requirements:
211
211
  - - "~>"
212
212
  - !ruby/object:Gem::Version
213
- version: '0.45'
213
+ version: 0.45.1
214
214
  - !ruby/object:Gem::Dependency
215
215
  name: ace-runtime
216
216
  requirement: !ruby/object:Gem::Requirement
@@ -245,14 +245,14 @@ dependencies:
245
245
  requirements:
246
246
  - - "~>"
247
247
  - !ruby/object:Gem::Version
248
- version: 0.5.1
248
+ version: '0.6'
249
249
  type: :runtime
250
250
  prerelease: false
251
251
  version_requirements: !ruby/object:Gem::Requirement
252
252
  requirements:
253
253
  - - "~>"
254
254
  - !ruby/object:Gem::Version
255
- version: 0.5.1
255
+ version: '0.6'
256
256
  - !ruby/object:Gem::Dependency
257
257
  name: ace-support-test-helpers
258
258
  requirement: !ruby/object:Gem::Requirement
@@ -418,6 +418,7 @@ files:
418
418
  - lib/ace/assign/atoms/step_numbering.rb
419
419
  - lib/ace/assign/atoms/step_sorter.rb
420
420
  - lib/ace/assign/atoms/tree_formatter.rb
421
+ - lib/ace/assign/authority/assignment_definition.rb
421
422
  - lib/ace/assign/authority/assignment_inventory.rb
422
423
  - lib/ace/assign/authority/campaign_consumer_policy.rb
423
424
  - lib/ace/assign/authority/campaign_execution.rb
@@ -460,6 +461,16 @@ files:
460
461
  - lib/ace/assign/authority/launch_steering.rb
461
462
  - lib/ace/assign/authority/launch_stop.rb
462
463
  - lib/ace/assign/authority/maintenance_workspace_retirement.rb
464
+ - lib/ace/assign/authority/ordinary_attempt_start.rb
465
+ - lib/ace/assign/authority/ordinary_client.rb
466
+ - lib/ace/assign/authority/ordinary_completion.rb
467
+ - lib/ace/assign/authority/ordinary_effect_settlement.rb
468
+ - lib/ace/assign/authority/ordinary_lifecycle.rb
469
+ - lib/ace/assign/authority/ordinary_peer.rb
470
+ - lib/ace/assign/authority/ordinary_progress.rb
471
+ - lib/ace/assign/authority/ordinary_queue.rb
472
+ - lib/ace/assign/authority/ordinary_results.rb
473
+ - lib/ace/assign/authority/ordinary_server.rb
463
474
  - lib/ace/assign/authority/original_inbox_selection.rb
464
475
  - lib/ace/assign/authority/posix_acl.rb
465
476
  - lib/ace/assign/authority/prepared_input.rb
@@ -478,6 +489,7 @@ files:
478
489
  - lib/ace/assign/authority/service_delivery_evidence.rb
479
490
  - lib/ace/assign/authority/service_evidence.rb
480
491
  - lib/ace/assign/authority/service_publication_evidence.rb
492
+ - lib/ace/assign/authority/socket_server.rb
481
493
  - lib/ace/assign/authority/task_context_entry.rb
482
494
  - lib/ace/assign/authority/transfer_codec.rb
483
495
  - lib/ace/assign/cli.rb
@@ -533,6 +545,7 @@ files:
533
545
  - lib/ace/assign/molecules/attempt_store.rb
534
546
  - lib/ace/assign/molecules/canonical_attempt_state.rb
535
547
  - lib/ace/assign/molecules/canonical_evidence.rb
548
+ - lib/ace/assign/molecules/canonical_read_commands.rb
536
549
  - lib/ace/assign/molecules/canonical_read_snapshot.rb
537
550
  - lib/ace/assign/molecules/evidence_calculator.rb
538
551
  - lib/ace/assign/molecules/evidence_journal.rb
@@ -540,6 +553,7 @@ files:
540
553
  - lib/ace/assign/molecules/execution_scope_lineage.rb
541
554
  - lib/ace/assign/molecules/fixed_effect_reference.rb
542
555
  - lib/ace/assign/molecules/fork_session_launcher.rb
556
+ - lib/ace/assign/molecules/installed_journal_read.rb
543
557
  - lib/ace/assign/molecules/journal_input_inhibition.rb
544
558
  - lib/ace/assign/molecules/journal_mutation.rb
545
559
  - lib/ace/assign/molecules/journal_prompt_mutation.rb