ace-assign 0.63.1 → 0.66.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  7. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  8. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  9. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  10. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  11. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  12. data/CHANGELOG.md +62 -0
  13. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  14. data/docs/protected-authority.md +282 -0
  15. data/docs/usage.md +205 -3
  16. data/exe/ace-scope-ready +55 -0
  17. data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
  18. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  19. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  20. data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
  21. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  22. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  23. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  24. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  25. data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
  26. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  27. data/lib/ace/assign/authority/client.rb +257 -0
  28. data/lib/ace/assign/authority/deployment.rb +627 -0
  29. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  30. data/lib/ace/assign/authority/endcap.rb +455 -0
  31. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  32. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  33. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  34. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  35. data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
  36. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  37. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  38. data/lib/ace/assign/authority/endcap_results.rb +398 -0
  39. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  40. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  41. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  42. data/lib/ace/assign/authority/endcap_services.rb +598 -0
  43. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  44. data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
  45. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  46. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  47. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  48. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  49. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  50. data/lib/ace/assign/authority/launch_driver.rb +393 -0
  51. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  52. data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
  53. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  54. data/lib/ace/assign/authority/launch_review.rb +91 -0
  55. data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
  56. data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
  57. data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
  58. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  59. data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
  60. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  61. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  62. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  63. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  64. data/lib/ace/assign/authority/posix_acl.rb +59 -0
  65. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  66. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  67. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  68. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  69. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  70. data/lib/ace/assign/authority/private_directory.rb +33 -0
  71. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  72. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  73. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  74. data/lib/ace/assign/authority/router.rb +76 -0
  75. data/lib/ace/assign/authority/server.rb +300 -0
  76. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  77. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  78. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  79. data/lib/ace/assign/authority/service_evidence.rb +286 -0
  80. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  81. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  82. data/lib/ace/assign/authority/transfer_codec.rb +187 -0
  83. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  84. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  85. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  86. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  87. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  88. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  89. data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
  90. data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
  91. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  92. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  93. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  94. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  95. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  96. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  97. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  98. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  99. data/lib/ace/assign/cli/commands/create.rb +11 -1
  100. data/lib/ace/assign/cli/commands/delivery.rb +107 -0
  101. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  102. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  103. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  104. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  105. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  106. data/lib/ace/assign/cli/commands/resume.rb +38 -0
  107. data/lib/ace/assign/cli/commands/select.rb +2 -0
  108. data/lib/ace/assign/cli/commands/start.rb +2 -0
  109. data/lib/ace/assign/cli/commands/status.rb +4 -2
  110. data/lib/ace/assign/cli/commands/step.rb +3 -1
  111. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  112. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  113. data/lib/ace/assign/cli.rb +56 -4
  114. data/lib/ace/assign/errors.rb +139 -0
  115. data/lib/ace/assign/models/assignment.rb +26 -4
  116. data/lib/ace/assign/models/evidence_event.rb +4 -2
  117. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  118. data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
  119. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  120. data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
  121. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  122. data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
  123. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  124. data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
  125. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  126. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  127. data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
  128. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  129. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
  130. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  131. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  132. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  133. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  134. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  135. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  136. data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
  137. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  138. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  139. data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
  140. data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
  141. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  142. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  143. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  144. data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
  145. data/lib/ace/assign/version.rb +1 -1
  146. data/lib/ace/assign.rb +4 -102
  147. metadata +186 -8
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ require_relative "protected_submission"
3
+ module Ace
4
+ module Assign
5
+ module CLI
6
+ module Commands
7
+ class SubmitCandidate < Ace::Support::Cli::Command
8
+ include Ace::Support::Cli::Base
9
+ include ProtectedSubmission
10
+ desc "Submit original protected worker bundle bytes"
11
+ option :mapping, desc: "Installed mapping"
12
+ option :assignment, desc: "Original assignment"
13
+ option :attempt, desc: "Original attempt"
14
+ option :head, desc: "Exact candidate head"
15
+ option :candidate_generation, desc: "Expected current candidate generation (zero before first submission)"
16
+ option :expected_generation, desc: "Persisted authority generation"
17
+ option :mutation, desc: "Stable original mutation"
18
+ option :bundle, desc: "Bounded local byte file"
19
+
20
+ def call(**options)
21
+ usage = "submit-candidate with original selectors, generation, mutation and --bundle FILE"
22
+ client, params, mutation = submission_request(options, usage)
23
+ head = require_option(options, :head, usage)
24
+ raise Ace::Support::Cli::Error, "Head must be an exact SHA-1" unless head.match?(/\A[0-9a-f]{40}\z/)
25
+ params.merge!("head" => head, "candidate_generation" => integer_option(options, :candidate_generation, usage))
26
+ bytes = input_bytes(require_option(options, :bundle, usage), limit: 64 * 1024 * 1024)
27
+ parts = [bytes]
28
+ emit_json(upload_call(client, "submit_candidate", params, mutation, parts, :candidate))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require_relative "protected_submission"
3
+ require_relative "../../authority/receipt_transfer"
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ class SubmitResult < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+ include ProtectedSubmission
11
+ desc "Submit original protected worker receipt bytes"
12
+ option :mapping, desc: "Installed mapping"
13
+ option :assignment, desc: "Original assignment"
14
+ option :attempt, desc: "Original attempt"
15
+ option :head, desc: "Exact candidate head"
16
+ option :candidate_generation, desc: "Positive candidate generation"
17
+ option :expected_generation, desc: "Persisted authority generation"
18
+ option :mutation, desc: "Stable original mutation"
19
+ option :receipt, desc: "Bounded local byte file"
20
+ option :artifact, type: :array, desc: "Ordered artifact byte files"
21
+
22
+ def call(**options)
23
+ usage = "submit-result with original selectors, generation, mutation and --receipt FILE"
24
+ client, params, mutation = submission_request(options, usage)
25
+ head = require_option(options, :head, usage)
26
+ raise Ace::Support::Cli::Error, "Head must be an exact SHA-1" unless head.match?(/\A[0-9a-f]{40}\z/)
27
+ params.merge!("head" => head, "candidate_generation" => integer_option(options, :candidate_generation, usage, positive: true))
28
+ bytes = input_bytes(require_option(options, :receipt, usage), limit: 16 * 1024)
29
+ receipt = input_json(bytes)
30
+ raise Ace::Support::Cli::Error, "Receipt must be an object" unless receipt.is_a?(Hash)
31
+ allowed = Models::ExecutionReceipt.from_h(receipt).to_h.keys
32
+ if (receipt.keys - allowed).any? || Models::ExecutionReceipt.forbidden_field?(receipt)
33
+ raise Ace::Support::Cli::Error, "Receipt contains unsupported fields"
34
+ end
35
+ unless receipt.values_at("assignment_id", "attempt_id", "head") == params.values_at("assignment_id", "attempt_id", "head") &&
36
+ Models::ExecutionReceipt::VERDICTS.include?(receipt["verdict"])
37
+ raise Ace::Support::Cli::Error, "Receipt identity or verdict differs"
38
+ end
39
+ files = options.fetch(:artifact, [])
40
+ unless files.is_a?(Array) && files.size <= 16 && !(receipt["verdict"] == "succeeded" && files.empty?)
41
+ raise Ace::Support::Cli::Error, "Receipt artifact declarations are invalid"
42
+ end
43
+ artifacts = files.map { |path| input_bytes(path, limit: 64 * 1024, nonempty: false) }
44
+ parts = [bytes, *artifacts]
45
+ digest = Digest::SHA256.hexdigest(bytes)
46
+ input = Struct.new(:parts) do
47
+ def count = parts.length
48
+ def bytes(index: 0) = parts.fetch(index)
49
+ end.new(parts)
50
+ # Reuse the maintained ordered artifact validator. This is input
51
+ # checking only; canonical result/producer admission remains Endcap's.
52
+ Ace::Assign::Authority::ReceiptTransfer.decode(input: input, receipt_sha256: digest,
53
+ artifact_field: "artifacts", reference_key: "path")
54
+ params["receipt_sha256"] = digest
55
+ emit_json(upload_call(client, "submit_result", params, mutation, parts, :receipt_artifacts))
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -44,6 +44,7 @@ require_relative "molecules/runtime_control_surface_runner"
44
44
  require_relative "molecules/preset_inferrer"
45
45
  require_relative "molecules/evidence_calculator"
46
46
  require_relative "molecules/evidence_journal"
47
+ require_relative "molecules/canonical_evidence"
47
48
  require_relative "molecules/execution_identity_resolver"
48
49
  require_relative "molecules/receipt_verifier"
49
50
  require_relative "molecules/attempt_reconciler"
@@ -53,9 +54,16 @@ require_relative "organisms/assignment_executor"
53
54
  require_relative "organisms/attempt_coordinator"
54
55
 
55
56
  # Commands
56
- require_relative "cli/commands/create"
57
+ require_relative "cli/commands/delivery"
57
58
  require_relative "cli/commands/assignment_target"
59
+ require_relative "cli/commands/create"
58
60
  require_relative "cli/commands/status"
61
+ require_relative "cli/commands/resume"
62
+ require_relative "cli/commands/inbox_bind"
63
+ require_relative "cli/commands/submit_candidate"
64
+ require_relative "cli/commands/submit_result"
65
+ require_relative "cli/commands/campaign_record_round"
66
+ require_relative "cli/commands/campaign_export_result"
59
67
  require_relative "cli/commands/step"
60
68
  require_relative "cli/commands/start"
61
69
  require_relative "cli/commands/finish"
@@ -66,6 +74,16 @@ require_relative "cli/commands/list"
66
74
  require_relative "cli/commands/select"
67
75
  require_relative "cli/commands/fork_run"
68
76
  require_relative "cli/commands/fork_session"
77
+ require_relative "cli/commands/authority/serve"
78
+ require_relative "cli/commands/authority/launch"
79
+ require_relative "cli/commands/authority/status"
80
+ require_relative "cli/commands/authority/terminate"
81
+ require_relative "cli/commands/authority/worker"
82
+ require_relative "cli/commands/authority/task_context"
83
+ require_relative "cli/commands/authority/task_context_principal"
84
+ require_relative "cli/commands/authority/task_context_selection"
85
+ require_relative "cli/commands/authority/inbox_context_principal"
86
+ require_relative "cli/commands/authority/inbox_context_selection"
69
87
  require_relative "cli/commands/attempt/base"
70
88
  require_relative "cli/commands/attempt/start"
71
89
  require_relative "cli/commands/attempt/status"
@@ -84,6 +102,21 @@ module Ace
84
102
  # Application commands with descriptions (for help output)
85
103
  REGISTERED_COMMANDS = [
86
104
  ["create", "Create assignment from preset or YAML"],
105
+ ["authority serve", "Serve the installed protected assignment owner"],
106
+ ["authority launch", "Launch one mapped native gated worker"],
107
+ ["authority status", "Inspect protected launch ownership"],
108
+ ["authority terminate", "Terminate the original native gated child"],
109
+ ["authority worker", "Consume original authenticated prepared work"],
110
+ ["authority task-context", "Read captured original task context through the fixed entry"],
111
+ ["authority task-context-principal", "Classify the actual caller through retained protected owners"],
112
+ ["authority task-context-selection", "Read the authenticated original context entry selection"],
113
+ ["authority inbox-context-principal", "Classify the actual caller through every retained protected owner"],
114
+ ["authority inbox-context-selection", "Read the fixed installed inbox context selection"],
115
+ ["delivery", "Execute or reconcile attempt-bound forge delivery"],
116
+ ["submit-candidate", "Submit original protected worker candidate bytes"],
117
+ ["submit-result", "Submit original protected worker receipt and artifact bytes"],
118
+ ["campaign-export-result", "Download the original parent campaign's verified accepted result"],
119
+ ["campaign-record-round", "Consume canonical settled children into the original campaign store"],
87
120
  ["status", "Show assignment status"],
88
121
  ["step", "Show step instructions"],
89
122
  ["start", "Start next workable step"],
@@ -97,8 +130,9 @@ module Ace
97
130
  ["attempt start", "Start a scoped attempt for an assignment step"],
98
131
  ["attempt status", "Show attempt status for an assignment"],
99
132
  ["attempt evidence", "Read accepted current-head check evidence"],
100
- ["attempt finish", "Finish an attempt with a structured execution receipt"],
101
- ["attempt reconcile", "Reconcile an interrupted or uncertain attempt"]
133
+ ["attempt finish", "Finish a protected canonical result or an ordinary local receipt"],
134
+ ["attempt reconcile", "Recover an exact protected attempt or reconcile an ordinary attempt"],
135
+ ["inbox-bind", "Bind an original protected Inbox registration"],
102
136
  ].freeze
103
137
 
104
138
  HELP_EXAMPLES = [
@@ -110,7 +144,8 @@ module Ace
110
144
  "cat report.md | ace-assign finish # Complete step via stdin",
111
145
  "ace-assign fork-run 010.01 # Run subtree in subprocess",
112
146
  "ace-assign attempt start --assignment ID --step 010 --project ID",
113
- "ace-assign attempt finish --attempt ID --receipt receipt.json"
147
+ "ace-assign attempt finish --attempt ID --receipt receipt.json",
148
+ "ace-assign resume --assignment ID --dry-run"
114
149
  ].freeze
115
150
 
116
151
  # Captured command exit code from last run
@@ -146,7 +181,18 @@ module Ace
146
181
  end
147
182
 
148
183
  # Register commands (wrapped to capture exit codes)
184
+ register "authority serve", wrap_command(Commands::Authority::Serve)
185
+ register "authority launch", wrap_command(Commands::Authority::Launch)
186
+ register "authority status", wrap_command(Commands::Authority::Status)
187
+ register "authority terminate", wrap_command(Commands::Authority::Terminate)
188
+ register "authority worker", wrap_command(Commands::Authority::Worker)
189
+ register "authority task-context", wrap_command(Commands::Authority::TaskContext)
190
+ register "authority task-context-principal", wrap_command(Commands::Authority::TaskContextPrincipal)
191
+ register "authority task-context-selection", wrap_command(Commands::Authority::TaskContextSelection)
192
+ register "authority inbox-context-principal", wrap_command(Commands::Authority::InboxContextPrincipal)
193
+ register "authority inbox-context-selection", wrap_command(Commands::Authority::InboxContextSelection)
149
194
  register "create", wrap_command(Commands::Create)
195
+ register "delivery", wrap_command(Commands::Delivery)
150
196
  register "status", wrap_command(Commands::Status)
151
197
  register "step", wrap_command(Commands::Step)
152
198
  register "start", wrap_command(Commands::Start)
@@ -163,6 +209,12 @@ module Ace
163
209
  register "attempt evidence", wrap_command(Commands::Attempt::Evidence)
164
210
  register "attempt finish", wrap_command(Commands::Attempt::Finish)
165
211
  register "attempt reconcile", wrap_command(Commands::Attempt::Reconcile)
212
+ register "resume", wrap_command(Commands::Resume)
213
+ register "inbox-bind", wrap_command(Commands::InboxBind)
214
+ register "submit-candidate", wrap_command(Commands::SubmitCandidate)
215
+ register "submit-result", wrap_command(Commands::SubmitResult)
216
+ register "campaign-export-result", wrap_command(Commands::CampaignExportResult)
217
+ register "campaign-record-round", wrap_command(Commands::CampaignRecordRound)
166
218
 
167
219
  # Register version command
168
220
  version_cmd = Ace::Support::Cli::VersionCommand.build(
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+ require "ace/support/cli/error"
3
+
4
+ module Ace
5
+ module Assign
6
+ # Base error class for all ace-assign exceptions.
7
+ # Inherits from Ace::Support::Cli::Error to support exception-based
8
+ # exit code pattern (per ADR-023).
9
+ #
10
+ # Subclasses should call super with appropriate exit_code.
11
+ class Error < Ace::Support::Cli::Error
12
+ def initialize(message, exit_code: 1)
13
+ super
14
+ end
15
+ end
16
+
17
+ # Assignment-related errors
18
+ module AssignmentErrors
19
+ class NotFound < Error
20
+ def initialize(message = "Assignment not found")
21
+ super(message, exit_code: 2)
22
+ end
23
+ end
24
+
25
+ class NoActive < Error
26
+ def initialize(message = "No active assignment")
27
+ super(message, exit_code: 2)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Config-related errors
33
+ module ConfigErrors
34
+ class NotFound < Error
35
+ def initialize(message = "Configuration not found")
36
+ super(message, exit_code: 3)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Step-related errors
42
+ module StepErrors
43
+ class NotFound < Error
44
+ def initialize(message = "Step not found")
45
+ super(message, exit_code: 4)
46
+ end
47
+ end
48
+
49
+ class InvalidState < Error; end
50
+ end
51
+
52
+ # Attempt/evidence-related errors (exit code 5)
53
+ module AttemptErrors
54
+ class InvalidTransition < Error
55
+ def initialize(message = "Illegal attempt state transition")
56
+ super(message, exit_code: 5)
57
+ end
58
+ end
59
+
60
+ class InvalidScope < Error
61
+ def initialize(message = "Invalid attempt scope")
62
+ super(message, exit_code: 5)
63
+ end
64
+ end
65
+
66
+ # Duplicate start with a conflicting binding for an owned subtree
67
+ class Conflict < Error
68
+ def initialize(message = "Conflicting active attempt for scope")
69
+ super(message, exit_code: 5)
70
+ end
71
+ end
72
+
73
+ class NotFound < Error
74
+ def initialize(message = "Attempt not found")
75
+ super(message, exit_code: 5)
76
+ end
77
+ end
78
+
79
+ # Untrusted or unknown execution identity; authority fails closed
80
+ class UnauthorizedIdentity < Error
81
+ def initialize(message = "Unknown or unauthorized execution identity")
82
+ super(message, exit_code: 5)
83
+ end
84
+ end
85
+
86
+ # Receipt failed verification (binding, head, digest, artifacts, review)
87
+ class ReceiptRejected < Error
88
+ def initialize(message = "Receipt rejected")
89
+ super(message, exit_code: 5)
90
+ end
91
+ end
92
+
93
+ # Byte framing failed before any business receipt could be admitted.
94
+ class MalformedTransfer < ReceiptRejected; end
95
+
96
+ # Recovery settled a prior effect, but did not perform this request.
97
+ class CurrentEffectRequired < ReceiptRejected
98
+ def initialize(operation:, recovered_operation:)
99
+ super("Recovered #{recovered_operation}; requested #{operation} still needs execution")
100
+ end
101
+ end
102
+
103
+ # Attempt state forbids the requested effect (terminal, uncertain, blocked)
104
+ class InvalidState < Error
105
+ def initialize(message = "Attempt state forbids the requested operation")
106
+ super(message, exit_code: 5)
107
+ end
108
+ end
109
+
110
+ # Evidence storage is missing or unwritable; external effects are blocked
111
+ class EvidenceUnavailable < Error
112
+ def initialize(message = "Evidence storage unavailable")
113
+ super(message, exit_code: 5)
114
+ end
115
+ end
116
+
117
+ # Complete maintenance exclusions are busy or their admission deadline expired.
118
+ class MaintenanceBusy < EvidenceUnavailable
119
+ end
120
+
121
+ # One canonical inventory row cannot fit the fixed protocol frame.
122
+ class BoundedResultUnavailable < EvidenceUnavailable
123
+ end
124
+
125
+ # Exhaustively authenticated service inventory remains unsettled.
126
+ class ServiceSettlementPending < EvidenceUnavailable
127
+ end
128
+
129
+ # Exhaustively authenticated current Inbox claims remain queued.
130
+ class InboxSettlementPending < EvidenceUnavailable
131
+ end
132
+
133
+ # Positively admitted original Inbox work remains pending, not corrupt.
134
+ class InboxContextPending < EvidenceUnavailable
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -20,7 +20,7 @@ module Ace
20
20
  # but tests would need to be updated to provide all required fields.
21
21
  class Assignment
22
22
  attr_reader :id, :name, :description, :created_at, :updated_at, :source_config, :cache_dir, :parent,
23
- :task_id, :project_id
23
+ :task_id, :project_id, :campaign_execution, :review_campaign
24
24
 
25
25
  # @param id [String] Unique assignment ID (6-char compact timestamp)
26
26
  # @param name [String] Human-readable assignment name
@@ -33,7 +33,7 @@ module Ace
33
33
  # @param task_id [String, nil] Source task ID attached to this assignment (nil = taskless)
34
34
  # @param project_id [String, nil] Project ID this assignment executes in
35
35
  def initialize(id:, name:, created_at:, source_config:, description: nil, updated_at: nil, cache_dir: nil,
36
- parent: nil, task_id: nil, project_id: nil)
36
+ parent: nil, task_id: nil, project_id: nil, campaign_execution: nil, review_campaign: nil)
37
37
  @id = id.freeze
38
38
  @name = name.freeze
39
39
  @description = description&.freeze
@@ -44,6 +44,24 @@ module Ace
44
44
  @parent = parent&.freeze
45
45
  @task_id = task_id&.freeze
46
46
  @project_id = project_id&.freeze
47
+ if !campaign_execution.nil? && !review_campaign.nil?
48
+ raise ArgumentError, "assignment cannot own both parent campaign and child execution"
49
+ end
50
+ @campaign_execution = immutable_campaign_execution(campaign_execution) unless campaign_execution.nil?
51
+ @review_campaign = immutable_campaign_execution(review_campaign) unless review_campaign.nil?
52
+ end
53
+
54
+ # Keep registered definition bytes stable without retaining caller-owned
55
+ # mutable objects. Authority validates the closed linkage and provenance.
56
+ private def immutable_campaign_execution(value)
57
+ case value
58
+ when Hash
59
+ value.to_h { |key, item| [immutable_campaign_execution(key), immutable_campaign_execution(item)] }.freeze
60
+ when Array then value.map { |item| immutable_campaign_execution(item) }.freeze
61
+ when String then value.dup.freeze
62
+ when Integer, NilClass, TrueClass, FalseClass then value
63
+ else raise ArgumentError, "invalid campaign execution value"
64
+ end
47
65
  end
48
66
 
49
67
  # @return [Boolean] True when a source task is attached; taskless
@@ -65,7 +83,9 @@ module Ace
65
83
  "source_config" => source_config,
66
84
  "parent" => parent,
67
85
  "task_id" => task_id,
68
- "project_id" => project_id
86
+ "project_id" => project_id,
87
+ "campaign_execution" => campaign_execution,
88
+ "review_campaign" => review_campaign
69
89
  }.compact
70
90
  end
71
91
 
@@ -84,7 +104,9 @@ module Ace
84
104
  cache_dir: cache_dir,
85
105
  parent: data["parent"],
86
106
  task_id: data["task_id"],
87
- project_id: data["project_id"]
107
+ project_id: data["project_id"],
108
+ campaign_execution: data["campaign_execution"],
109
+ review_campaign: data["review_campaign"]
88
110
  )
89
111
  end
90
112
 
@@ -15,9 +15,11 @@ module Ace
15
15
  # metadata only.
16
16
  module EvidenceEvent
17
17
  TYPES = %w[
18
- intent process_start receipt_accepted transition
18
+ intent process_start receipt_accepted transition attempt_stopped
19
19
  candidate_invalidated reconciliation
20
- service_claim service_transition
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
21
23
  ].freeze
22
24
 
23
25
  # Build an event chained to a previous digest.
@@ -13,7 +13,8 @@ module Ace
13
13
  # Uses ace-b36ts for assignment ID generation.
14
14
  class AssignmentManager
15
15
  # @param cache_base [String] Base cache directory
16
- def initialize(cache_base: nil)
16
+ def initialize(cache_base: nil, selected_assignment: nil)
17
+ @selected_assignment = selected_assignment
17
18
  @cache_base = cache_base || Ace::Assign.cache_dir
18
19
  end
19
20
 
@@ -85,6 +86,7 @@ module Ace
85
86
  #
86
87
  # @return [Models::Assignment, nil] Most recent assignment or nil
87
88
  def find_active
89
+ return @selected_assignment if @selected_assignment
88
90
  return nil unless File.directory?(@cache_base)
89
91
 
90
92
  # Priority 1: use .current symlink if it exists (explicit selection)
@@ -185,6 +187,7 @@ module Ace
185
187
  )
186
188
 
187
189
  write_assignment_file(updated)
190
+ @selected_assignment = updated if @selected_assignment && @selected_assignment.id == updated.id
188
191
 
189
192
  # Update .latest symlink since this assignment was just updated
190
193
  update_latest_symlink(assignment.id)
@@ -259,7 +262,14 @@ module Ace
259
262
 
260
263
  def write_assignment_file(assignment)
261
264
  assignment_file = File.join(assignment.cache_dir, "assignment.yaml")
262
- File.write(assignment_file, assignment.to_h.to_yaml)
265
+ if @selected_assignment
266
+ File.open(assignment_file, File::WRONLY | File::TRUNC | File::NOFOLLOW | File::NONBLOCK) do |file|
267
+ raise ArgumentError, "selected assignment metadata is not regular" unless file.stat.file?
268
+ file.write(assignment.to_h.to_yaml); file.flush; file.fsync
269
+ end
270
+ else
271
+ File.write(assignment_file, assignment.to_h.to_yaml)
272
+ end
263
273
  end
264
274
 
265
275
  def load_from_file(assignment_file)
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "canonical_attempt_state"
3
+
4
+ require "json"
2
5
 
3
6
  module Ace
4
7
  module Assign
@@ -6,7 +9,7 @@ module Ace
6
9
  # Conservative classification and resolution of interrupted attempts.
7
10
  #
8
11
  # Classification of a `running` attempt found after a lost session:
9
- # - no recorded process start -> `stopped` (nothing could have run)
12
+ # - no recorded process start -> `uncertain` (launch may be unbound)
10
13
  # - recorded process still verifiably live -> keep `running`
11
14
  # - otherwise -> `uncertain` (the operation may have completed without
12
15
  # a durable receipt)
@@ -17,11 +20,28 @@ module Ace
17
20
  # effects are never retried automatically, and an uncertain attempt
18
21
  # without a receipt stays uncertain.
19
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
+
20
38
  # @param journal [EvidenceJournal, nil] Managed evidence journal
21
39
  # @param verifier [ReceiptVerifier, nil] Receipt verifier (coordinator supplies on use)
22
- def initialize(journal: nil, verifier: nil)
40
+ def initialize(journal: nil, verifier: nil, observer: nil, runtime_resolver: Ace::Runtime)
23
41
  @journal = journal
24
42
  @verifier = verifier
43
+ @observer = observer || Ace::Runtime::Molecules::ProcessIdentity.new
44
+ @runtime_resolver = runtime_resolver
25
45
  end
26
46
 
27
47
  # Classify a running attempt after an interruption.
@@ -29,9 +49,11 @@ module Ace
29
49
  # @param attempt [Models::Attempt] Running attempt
30
50
  # @return [Symbol] :stopped, :live, or :uncertain
31
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
32
56
  return :live if process_live?(attempt)
33
- return :stopped unless process_started?(attempt)
34
-
35
57
  :uncertain
36
58
  end
37
59
 
@@ -44,15 +66,27 @@ module Ace
44
66
  # @param attempt [Models::Attempt] Attempt to inspect
45
67
  # @return [Boolean] True when the recorded process is verifiably alive
46
68
  def process_live?(attempt)
47
- pid = start_event(attempt)&.dig("payload", "pid")
48
- return false unless pid
69
+ observation(attempt)["liveness"] == "live"
70
+ end
49
71
 
50
- Process.kill(0, pid)
51
- true
52
- rescue Errno::EPERM
53
- true
54
- rescue Errno::ESRCH, TypeError, ArgumentError
55
- false
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)
56
90
  end
57
91
 
58
92
  # Runtime recorded at the attempt's process start — the boundary any
@@ -64,6 +98,11 @@ module Ace
64
98
  start_event(attempt)&.dig("payload", "runtime")
65
99
  end
66
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
+
67
106
  private
68
107
 
69
108
  def start_event(attempt)