ace-assign 0.71.0 → 0.73.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +5 -2116
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Models
6
- # Immutable start-time facts for an assignment attempt.
7
- #
8
- # A binding freezes exactly what an attempt owns: the immutable attempt
9
- # ID, the assignment and step/subtree scope, the project, the
10
- # authenticated actor/role and runtime identity resolved at the
11
- # execution boundary, the source task ID, the base head captured once at
12
- # start, and the configured evidence ref. Bindings are never mutated;
13
- # changes require a new attempt.
14
- class AttemptBinding
15
- attr_reader :attempt_id, :assignment_id, :scope, :project_id, :task_id,
16
- :actor, :role, :runtime, :base_head, :evidence_git_ref, :created_at
17
-
18
- # @param attempt_id [String] Immutable attempt ID (ace-b36ts)
19
- # @param assignment_id [String] Owning assignment ID
20
- # @param scope [String] Canonical step/subtree scope
21
- # @param project_id [String] Project the attempt executes in
22
- # @param actor [String] Authenticated actor from the execution boundary
23
- # @param role [String] Trusted role (coordinator, service, worker)
24
- # @param runtime [String] Runtime identity of the executing boundary
25
- # @param base_head [String] Git HEAD captured exactly once at start
26
- # @param task_id [String, nil] Source task ID (nil for taskless assignments)
27
- # @param evidence_git_ref [String, nil] Evidence ref for managed attempts
28
- # @param created_at [Time] Binding creation time
29
- def initialize(attempt_id:, assignment_id:, scope:, project_id:, actor:, role:, runtime:, base_head:,
30
- task_id: nil, evidence_git_ref: nil, created_at:)
31
- @attempt_id = attempt_id.freeze
32
- @assignment_id = assignment_id.freeze
33
- @scope = scope.freeze
34
- @project_id = project_id.freeze
35
- @task_id = task_id&.freeze
36
- @actor = actor.freeze
37
- @role = role.freeze
38
- @runtime = runtime.freeze
39
- @base_head = base_head.freeze
40
- @evidence_git_ref = evidence_git_ref&.freeze
41
- @created_at = created_at
42
- end
43
-
44
- # @return [Boolean] True when the attempt is bound to a source task and
45
- # eligible for managed evidence-ref delivery
46
- def managed?
47
- !task_id.nil? && !task_id.to_s.strip.empty?
48
- end
49
-
50
- # Recovery mode implied by task attachment: git-backed journaling for
51
- # task-attached attempts, disposable local state otherwise.
52
- #
53
- # @return [String] "git" or "local_only"
54
- def recovery_mode
55
- managed? ? "git" : "local_only"
56
- end
57
-
58
- # Identity facts repeated by receipts must match the binding exactly.
59
- #
60
- # @param other [AttemptBinding] Binding to compare
61
- # @return [Boolean] True if both bindings own the same attempt identity
62
- def same_identity?(other)
63
- attempt_id == other.attempt_id &&
64
- assignment_id == other.assignment_id &&
65
- scope == other.scope &&
66
- project_id == other.project_id
67
- end
68
-
69
- # Convert to a hash for canonical serialization.
70
- # @return [Hash] Binding data with string keys
71
- def to_h
72
- {
73
- "attempt_id" => attempt_id,
74
- "assignment_id" => assignment_id,
75
- "scope" => scope,
76
- "project_id" => project_id,
77
- "task_id" => task_id,
78
- "actor" => actor,
79
- "role" => role,
80
- "runtime" => runtime,
81
- "base_head" => base_head,
82
- "evidence_git_ref" => evidence_git_ref,
83
- "created_at" => created_at.iso8601
84
- }
85
- end
86
-
87
- # Rebuild a binding from serialized data.
88
- #
89
- # @param data [Hash] Serialized binding (string keys)
90
- # @return [AttemptBinding] Rebuilt binding
91
- def self.from_h(data)
92
- new(
93
- attempt_id: data["attempt_id"],
94
- assignment_id: data["assignment_id"],
95
- scope: data["scope"],
96
- project_id: data["project_id"],
97
- task_id: data["task_id"],
98
- actor: data["actor"],
99
- role: data["role"],
100
- runtime: data["runtime"],
101
- base_head: data["base_head"],
102
- evidence_git_ref: data["evidence_git_ref"],
103
- created_at: parse_time(data["created_at"])
104
- )
105
- end
106
-
107
- def self.parse_time(value)
108
- return value if value.is_a?(Time)
109
- require "time"
110
- Time.parse(value)
111
- end
112
- private_class_method :parse_time
113
- end
114
- end
115
- end
116
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../atoms/evidence_digest"
4
-
5
- module Ace
6
- module Assign
7
- module Models
8
- # Append-only accepted evidence event.
9
- #
10
- # Evidence events are the journal trail of an attempt: intent,
11
- # process start, accepted receipts, state transitions, candidate
12
- # invalidations, and reconciliations. Each event chains to the digest
13
- # of its predecessor for the attempt, so accepted history cannot be
14
- # reordered or silently rewritten. Events carry bounded, non-secret
15
- # metadata only.
16
- module EvidenceEvent
17
- TYPES = %w[
18
- intent process_start receipt_accepted transition attempt_stopped
19
- candidate_invalidated reconciliation
20
- service_claim service_transition service_challenge service_no_effect_challenge service_publication_challenge service_publication_issuing recovery_observation inbox_binding delivery
21
- authority_mutation evidence_import proposal_state result_submitted prompt_issued prompt_outcome prompt_completion_observed input_inhibited
22
- scope_provisioning scope_bound scope_native_bound scope_child_bound scope_sealed scope_closed_no_writers
23
- merge_admission merge_transition
24
- ].freeze
25
-
26
- # Build an event chained to a previous digest.
27
- #
28
- # @param type [String] Event type (see TYPES)
29
- # @param attempt_id [String] Owning attempt ID
30
- # @param payload [Hash] Bounded, non-secret event metadata
31
- # @param previous_digest [String, nil] Digest of the previous event (chain)
32
- # @param recorded_at [Time] Event creation time
33
- # @return [Hash] Serializable event with type, payload, digests, timestamp
34
- # @raise [ArgumentError] for unknown event types
35
- def self.build(type:, attempt_id:, payload:, previous_digest: nil, recorded_at: Time.now.utc)
36
- raise ArgumentError, "Unknown evidence event type: #{type}" unless TYPES.include?(type.to_s)
37
-
38
- recorded_at = recorded_at.utc
39
- body = {
40
- "type" => type.to_s,
41
- "attempt_id" => attempt_id,
42
- "payload" => payload,
43
- "previous_digest" => previous_digest,
44
- "recorded_at" => recorded_at.iso8601
45
- }
46
- body.merge("digest" => Atoms::EvidenceDigest.digest(body))
47
- end
48
-
49
- # Validate that a stored event is intact: its digest must match a
50
- # recomputation over its own body.
51
- #
52
- # @param event [Hash] Stored event
53
- # @return [Boolean] True if the event digest verifies
54
- def self.valid?(event)
55
- stored = event["digest"]
56
- return false if stored.nil?
57
-
58
- body = {
59
- "type" => event["type"],
60
- "attempt_id" => event["attempt_id"],
61
- "payload" => event["payload"],
62
- "previous_digest" => event["previous_digest"],
63
- "recorded_at" => event["recorded_at"]
64
- }
65
- Atoms::EvidenceDigest.digest(body) == stored
66
- end
67
-
68
- # Validate a whole chain: every event verifies and links to its
69
- # predecessor.
70
- #
71
- # @param events [Array<Hash>] Ordered event list
72
- # @return [Boolean] True if the chain is intact
73
- def self.chain_valid?(events)
74
- previous = nil
75
- events.all? do |event|
76
- return false unless valid?(event)
77
- return false unless event["previous_digest"] == previous
78
-
79
- previous = event["digest"]
80
- true
81
- end
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -1,139 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../atoms/evidence_digest"
4
-
5
- module Ace
6
- module Assign
7
- module Models
8
- # Structured, non-secret execution receipt.
9
- #
10
- # A receipt is the only accepted proof that an attempted operation
11
- # completed: it names the attributed producer, the attempted operation,
12
- # the actual tested/reviewed head, the scope, a verdict, the required
13
- # artifacts with SHA-256 digests, and executed checks. Receipts never
14
- # carry credentials, terminal bytes, or receipt-file payloads; journal
15
- # entries keep metadata and digests only.
16
- class ExecutionReceipt
17
- VERDICTS = %w[succeeded failed].freeze
18
-
19
- # Field names rejected outright in receipt payloads: accepted evidence
20
- # is structured metadata, never raw process output or credentials.
21
- FORBIDDEN_FIELDS = %w[
22
- stdout stderr terminal_output log logs credentials secret secrets
23
- token password api_key private_key env environment
24
- ].freeze
25
-
26
- attr_reader :attempt_id, :assignment_id, :project_id, :scope, :operation,
27
- :producer, :head, :verdict, :artifacts, :checks, :review, :campaign, :recorded_at, :digest
28
-
29
- # @param attempt_id [String] Attempt the receipt belongs to
30
- # @param assignment_id [String] Owning assignment ID
31
- # @param project_id [String] Project the operation ran in
32
- # @param scope [String] Canonical step/subtree scope
33
- # @param operation [String] Attempted operation name
34
- # @param producer [Hash] Attributed producer (actor, role, runtime)
35
- # @param head [String] Actual tested/reviewed head
36
- # @param verdict [String] "succeeded" or "failed"
37
- # @param artifacts [Array<Hash>] Artifacts with path + sha256
38
- # @param checks [Array<Hash>] Executed checks with name + verdict
39
- # @param review [Hash, nil] Reviewer verdict for review receipts
40
- # @param recorded_at [Time] Receipt creation time
41
- # @param digest [String, nil] Canonical digest (computed when nil)
42
- def initialize(attempt_id:, assignment_id:, project_id:, scope:, operation:, producer:, head:, verdict:,
43
- artifacts: [], checks: [], review: nil, campaign: nil, recorded_at:, digest: nil)
44
- @attempt_id = attempt_id
45
- @assignment_id = assignment_id
46
- @project_id = project_id
47
- @scope = scope
48
- @operation = operation
49
- @producer = producer
50
- @head = head
51
- @verdict = verdict
52
- @artifacts = artifacts
53
- @checks = checks
54
- @review = review
55
- @campaign = campaign
56
- @recorded_at = recorded_at
57
- @digest = digest || Atoms::EvidenceDigest.digest(digest_payload)
58
- end
59
-
60
- # Canonical payload covered by the receipt digest (excludes the digest
61
- # itself and the record timestamp, which is assigned at acceptance).
62
- #
63
- # @return [Hash] Digest payload
64
- def digest_payload
65
- payload = {
66
- "attempt_id" => attempt_id,
67
- "assignment_id" => assignment_id,
68
- "project_id" => project_id,
69
- "scope" => scope,
70
- "operation" => operation,
71
- "producer" => producer,
72
- "head" => head,
73
- "verdict" => verdict,
74
- "artifacts" => artifacts,
75
- "checks" => checks,
76
- "review" => review
77
- }
78
- payload["campaign"] = campaign if campaign
79
- payload
80
- end
81
-
82
- # Convert to a hash for serialization.
83
- # @return [Hash] Receipt data with string keys
84
- def to_h
85
- digest_payload.merge(
86
- "recorded_at" => recorded_at.iso8601,
87
- "digest" => digest
88
- )
89
- end
90
-
91
- # Rebuild a receipt from serialized or submitted data.
92
- #
93
- # @param data [Hash] Receipt data (string keys); missing digest is computed
94
- # @return [ExecutionReceipt] Receipt instance
95
- def self.from_h(data)
96
- new(
97
- attempt_id: data["attempt_id"],
98
- assignment_id: data["assignment_id"],
99
- project_id: data["project_id"],
100
- scope: data["scope"],
101
- operation: data["operation"],
102
- producer: data["producer"] || {},
103
- head: data["head"],
104
- verdict: data["verdict"],
105
- artifacts: data["artifacts"] || [],
106
- checks: data["checks"] || [],
107
- review: data["review"],
108
- campaign: data["campaign"],
109
- recorded_at: parse_time(data["recorded_at"]) || Time.now.utc,
110
- digest: data["digest"]
111
- )
112
- end
113
-
114
- # Reject payloads that carry forbidden fields anywhere in the tree.
115
- #
116
- # @param data [Hash] Raw submitted receipt data
117
- # @return [Boolean] True if a forbidden field is present
118
- def self.forbidden_field?(data)
119
- case data
120
- when Hash
121
- data.any? { |key, value| FORBIDDEN_FIELDS.include?(key.to_s) || forbidden_field?(value) }
122
- when Array
123
- data.any? { |item| forbidden_field?(item) }
124
- else
125
- false
126
- end
127
- end
128
-
129
- def self.parse_time(value)
130
- return nil if value.nil?
131
- return value if value.is_a?(Time)
132
- require "time"
133
- Time.parse(value)
134
- end
135
- private_class_method :parse_time
136
- end
137
- end
138
- end
139
- end
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative "canonical_attempt_state"
3
-
4
- require "json"
5
-
6
- module Ace
7
- module Assign
8
- module Molecules
9
- # Conservative classification and resolution of interrupted attempts.
10
- #
11
- # Classification of a `running` attempt found after a lost session:
12
- # - no recorded process start -> `uncertain` (launch may be unbound)
13
- # - recorded process still verifiably live -> keep `running`
14
- # - otherwise -> `uncertain` (the operation may have completed without
15
- # a durable receipt)
16
- #
17
- # Reconciliation resolves an `uncertain` attempt only against verified
18
- # receipts whose producer runtime matches the recorded execution
19
- # boundary. It never replays operations: merge, publish, and deploy
20
- # effects are never retried automatically, and an uncertain attempt
21
- # without a receipt stays uncertain.
22
- class AttemptReconciler
23
- # The protected boundary supplies authenticated original scope facts
24
- # and exact kernel identities; never a runtime discovered by pane name.
25
- def self.protected_observation(kernel:, binding:, observer:, lineage:)
26
- return {"liveness" => "unknown"} if lineage.sealed?
27
- kernel.live!(binding.fetch("process_identity"))
28
- kernel.live!(binding.fetch("native_origin").fetch("server_identity"))
29
- observation = observer.observe(lineage)
30
- unless observation.is_a?(Hash) && observation["populated"].is_a?(Integer) && observation["populated"].positive?
31
- return {"liveness" => "unknown"}
32
- end
33
- {"liveness" => "live", "identity" => binding.fetch("process_identity"), "runtime_binding" => binding}
34
- rescue AttemptErrors::EvidenceUnavailable, Ace::Runtime::RuntimeUnavailableError
35
- {"liveness" => "unknown"}
36
- end
37
-
38
- # @param journal [EvidenceJournal, nil] Managed evidence journal
39
- # @param verifier [ReceiptVerifier, nil] Receipt verifier (coordinator supplies on use)
40
- def initialize(journal: nil, verifier: nil, observer: nil, runtime_resolver: Ace::Runtime)
41
- @journal = journal
42
- @verifier = verifier
43
- @observer = observer || Ace::Runtime::Molecules::ProcessIdentity.new
44
- @runtime_resolver = runtime_resolver
45
- end
46
-
47
- # Classify a running attempt after an interruption.
48
- #
49
- # @param attempt [Models::Attempt] Running attempt
50
- # @return [Symbol] :stopped, :live, or :uncertain
51
- def classify(attempt)
52
- events = events_for(attempt)
53
- if events.any? { |event| event["type"] == "attempt_stopped" } && CanonicalAttemptState.derive(events) == "stopped"
54
- return :stopped
55
- end
56
- return :live if process_live?(attempt)
57
- :uncertain
58
- end
59
-
60
- # @param attempt [Models::Attempt] Attempt to inspect
61
- # @return [Boolean] True when a process start was recorded
62
- def process_started?(attempt)
63
- !start_event(attempt).nil?
64
- end
65
-
66
- # @param attempt [Models::Attempt] Attempt to inspect
67
- # @return [Boolean] True when the recorded process is verifiably alive
68
- def process_live?(attempt)
69
- observation(attempt)["liveness"] == "live"
70
- end
71
-
72
- def observation(attempt)
73
- payload = start_event(attempt)&.dig("payload")
74
- observation = @observer.observe(payload&.dig("process_identity"))
75
- binding = payload&.dig("runtime_binding")
76
- return observation unless binding && observation["liveness"] == "live"
77
-
78
- live = @runtime_resolver.resolve(binding["runtime"]).process_binding(
79
- pane: binding["pane"], caller_pid: binding.dig("process_identity", "pid"))
80
- return observation if binding == live
81
-
82
- observation.merge("liveness" => "unknown", "reason" => "native process/session binding changed")
83
- rescue Ace::Runtime::Error
84
- {"liveness" => "unknown", "observed_at" => Time.now.utc.iso8601,
85
- "reason" => "native process/session observation unavailable"}
86
- end
87
-
88
- def events(attempt)
89
- events_for(attempt)
90
- end
91
-
92
- # Runtime recorded at the attempt's process start — the boundary any
93
- # reconciling receipt must attribute to.
94
- #
95
- # @param attempt [Models::Attempt] Attempt to inspect
96
- # @return [String, nil] Recorded runtime identity
97
- def recorded_runtime(attempt)
98
- start_event(attempt)&.dig("payload", "runtime")
99
- end
100
-
101
- def recorded_binding(attempt)
102
- binding = start_event(attempt)&.dig("payload", "runtime_binding")
103
- JSON.parse(JSON.generate(binding)) if binding.is_a?(Hash)
104
- end
105
-
106
- private
107
-
108
- def start_event(attempt)
109
- events_for(attempt).reverse.find do |event|
110
- event["type"] == "process_start" && event["attempt_id"] == attempt.attempt_id
111
- end
112
- end
113
-
114
- def events_for(attempt)
115
- events = if attempt.managed?
116
- (@journal || Molecules::EvidenceJournal.new(repo_root: Dir.pwd))
117
- .read_events(attempt.binding.assignment_id)
118
- else
119
- attempt.events
120
- end
121
- # Only this attempt's events define its execution boundary.
122
- events.select { |event| event["attempt_id"] == attempt.attempt_id }
123
- end
124
- end
125
- end
126
- end
127
- end
@@ -1,201 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "json"
5
-
6
- module Ace
7
- module Assign
8
- module Molecules
9
- # Durable local attempt state: the single authority for attempt records
10
- # and active subtree ownership.
11
- #
12
- # Layout under the assignment cache directory (`.ace-local/assign` by
13
- # default, which remains disposable session/projection data):
14
- # - `<assignment-id>/attempts/records/<attempt-id>.json` — full attempt
15
- # record, written atomically (temp file + rename).
16
- # - `<assignment-id>/attempts/active/<ownership-key>.json` — pointer to
17
- # the one active attempt owning an assignment + normalized subtree.
18
- # - `<assignment-id>/attempts/.lock` — flock serialization for
19
- # claim/release and record transitions.
20
- #
21
- # Only one active attempt owns a subtree; claim replaces the pointer
22
- # only for the identical binding, and release clears it only when it
23
- # still references the given attempt.
24
- class AttemptStore
25
- # @param cache_base [String] Base cache directory
26
- def initialize(cache_base: nil)
27
- @cache_base = cache_base || Ace::Assign.cache_dir
28
- end
29
-
30
- # @param assignment_id [String] Assignment ID
31
- # @return [String] Attempts directory for the assignment
32
- def attempts_dir(assignment_id)
33
- File.join(@cache_base, assignment_id, "attempts")
34
- end
35
-
36
- # Serialize a block under the assignment's exclusive attempt lock.
37
- #
38
- # @param assignment_id [String] Assignment ID
39
- # @yield Block executed while holding the lock
40
- def with_lock(assignment_id)
41
- FileUtils.mkdir_p(attempts_dir(assignment_id))
42
- File.open(lock_path(assignment_id), File::RDWR | File::CREAT) do |lock|
43
- lock.flock(File::LOCK_EX)
44
- begin
45
- yield
46
- ensure
47
- lock.flock(File::LOCK_UN)
48
- end
49
- end
50
- end
51
-
52
- # Persist an attempt record atomically.
53
- #
54
- # @param attempt [Models::Attempt] Attempt to persist
55
- def save(attempt)
56
- FileUtils.mkdir_p(records_dir(attempt.binding.assignment_id))
57
- atomic_write(
58
- record_path(attempt.binding.assignment_id, attempt.attempt_id),
59
- JSON.pretty_generate(attempt.to_h)
60
- )
61
- end
62
-
63
- # Load an attempt record.
64
- #
65
- # @param assignment_id [String] Assignment ID
66
- # @param attempt_id [String] Attempt ID
67
- # @return [Models::Attempt, nil] Attempt or nil when absent/corrupt
68
- def load(assignment_id, attempt_id)
69
- path = record_path(assignment_id, attempt_id)
70
- return nil unless File.exist?(path)
71
-
72
- Models::Attempt.from_h(JSON.parse(File.read(path)))
73
- rescue JSON::ParserError, KeyError, ArgumentError, TypeError => e
74
- warn "ace-assign: corrupt attempt record #{path}: #{e.message}" if Ace::Assign.debug?
75
- nil
76
- end
77
-
78
- # The active attempt owning an assignment + scope, if any.
79
- #
80
- # @param assignment_id [String] Assignment ID
81
- # @param scope [String] Step/subtree scope
82
- # @return [Models::Attempt, nil] Active attempt or nil
83
- def active(assignment_id, scope)
84
- pointer = active_path(assignment_id, scope)
85
- return nil unless File.exist?(pointer)
86
-
87
- entry = JSON.parse(File.read(pointer))
88
- attempt = load(assignment_id, entry["attempt_id"])
89
- return nil unless attempt&.active?
90
-
91
- attempt
92
- rescue JSON::ParserError, TypeError
93
- nil
94
- end
95
-
96
- # Claim subtree ownership for an attempt. Callers must hold
97
- # {with_lock} for read-modify-write atomicity.
98
- #
99
- # @param assignment_id [String] Assignment ID
100
- # @param scope [String] Canonical scope
101
- # @param attempt [Models::Attempt] Active attempt claiming the scope
102
- def claim(assignment_id, scope, attempt)
103
- FileUtils.mkdir_p(active_dir(assignment_id))
104
- atomic_write(
105
- active_path(assignment_id, scope),
106
- JSON.generate({"attempt_id" => attempt.attempt_id})
107
- )
108
- end
109
-
110
- # Release subtree ownership when it still references the attempt.
111
- #
112
- # @param assignment_id [String] Assignment ID
113
- # @param scope [String] Canonical scope
114
- # @param attempt_id [String] Attempt releasing ownership
115
- def release(assignment_id, scope, attempt_id)
116
- pointer = active_path(assignment_id, scope)
117
- return unless File.exist?(pointer)
118
-
119
- entry = JSON.parse(File.read(pointer))
120
- File.delete(pointer) if entry["attempt_id"] == attempt_id
121
- rescue JSON::ParserError, TypeError
122
- nil
123
- end
124
-
125
- # Atomically reserve an attempt ID across all assignments
126
- # (mkdir-based EEXIST check). Returns false when already taken.
127
- #
128
- # @param attempt_id [String] Candidate ID
129
- # @return [Boolean] True when newly reserved
130
- def reserve_id(attempt_id)
131
- registry = File.join(@cache_base, ".attempt-ids")
132
- FileUtils.mkdir_p(registry)
133
- Dir.mkdir(File.join(registry, attempt_id))
134
- true
135
- rescue Errno::EEXIST
136
- false
137
- end
138
-
139
- # Locate an attempt record by ID across all assignments (used by
140
- # reconcile, which receives only the attempt ID).
141
- #
142
- # @param attempt_id [String] Attempt ID
143
- # @return [Models::Attempt, nil] Attempt or nil
144
- def find(attempt_id)
145
- path = Dir.glob(File.join(@cache_base, "*", "attempts", "records", "#{attempt_id}.json")).first
146
- return nil unless path
147
-
148
- assignment_id = File.basename(File.dirname(File.dirname(File.dirname(path))))
149
- load(assignment_id, attempt_id)
150
- end
151
-
152
- # All attempt records for an assignment, newest first. Ties (records
153
- # serialized within the same second) break deterministically by
154
- # attempt ID.
155
- #
156
- # @param assignment_id [String] Assignment ID
157
- # @return [Array<Models::Attempt>] Persisted attempts
158
- def list(assignment_id)
159
- Dir.glob(File.join(records_dir(assignment_id), "*.json"))
160
- .map { |path| load(assignment_id, File.basename(path, ".json")) }
161
- .compact
162
- .sort_by { |attempt| [attempt.updated_at.utc, attempt.attempt_id] }
163
- .reverse
164
- end
165
-
166
- private
167
-
168
- def records_dir(assignment_id)
169
- File.join(attempts_dir(assignment_id), "records")
170
- end
171
-
172
- def active_dir(assignment_id)
173
- File.join(attempts_dir(assignment_id), "active")
174
- end
175
-
176
- def lock_path(assignment_id)
177
- File.join(attempts_dir(assignment_id), ".lock")
178
- end
179
-
180
- def record_path(assignment_id, attempt_id)
181
- File.join(records_dir(assignment_id), "#{attempt_id}.json")
182
- end
183
-
184
- def active_path(assignment_id, scope)
185
- File.join(active_dir(assignment_id), Atoms::AssignmentScope.ownership_key(assignment_id, scope))
186
- end
187
-
188
- # Atomic same-filesystem write: temp file plus rename.
189
- def atomic_write(path, content)
190
- dir = File.dirname(path)
191
- FileUtils.mkdir_p(dir)
192
- temp = File.join(dir, ".tmp-#{Process.pid}-#{rand(1_000_000)}")
193
- File.write(temp, content)
194
- File.rename(temp, path)
195
- ensure
196
- File.delete(temp) if temp && File.exist?(temp)
197
- end
198
- end
199
- end
200
- end
201
- end