ace-assign 0.58.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c66fd1fc72e1bb186537931c960477dc7f9405ce3bc0f41c195a4b6601aa9bd1
4
- data.tar.gz: fef52c4c68bbb1435499d7cb0468d09919b5b330d9d7226b4c676c54c12ff9ad
3
+ metadata.gz: 494e61c0842f8930425d12aeee6c0b51b46ce491e829c19401df1296e440ca09
4
+ data.tar.gz: 614e997a57ea09e7ff83d30ad372514dc0c0abceac7a809cdcb4aea1b630005a
5
5
  SHA512:
6
- metadata.gz: 8e31f4379b900c7906be0b15b92768c904d57bf3fcfccbaf284ca972168413be443907281e1f1b939dff66337daf2835f32fc27c27fc802738dbe19238f220c4
7
- data.tar.gz: f77a7e04992fddf9b8ddf99e57243759e1ec8ccbd7502fbae9836e32bd749a60b5feb7d7c4a63017f92b384efdce49654547b7ecb1d18fb29c66cab644dad18a
6
+ metadata.gz: a83405e18f47bdf33cf83619a88b790ad6ed27ff933f8816530931a2eb571c744f8eeca5c25aded6c0c2dacdda5429aec87372aadae63b0393a17a2797d149ea
7
+ data.tar.gz: c6dba5cc8f2925af84265864bbf4b54e3102f27d0707ee936d0440ec3df28e3acbf67cc7040a103c808461d33cfd77c8dd43f5092bbec6ce0fa362b1920cfc52
data/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.61.0] - 2026-10-02
11
+
12
+ ### Added
13
+
14
+ - Journal-backed service request claims, terminal receipts, and uncertainty
15
+ tracking. Assignment evidence now exposes unresolved service effects and
16
+ the latest evidence ref commit without changing the candidate head.
17
+
18
+ ### Fixed
19
+
20
+ - Validate terminal service receipts against the exact claimed request and
21
+ executor evidence before accepting them in the assignment journal.
22
+
23
+ ### Technical
24
+
25
+ - Updated the ace-tmux dependency to the 0.18 adapter release line.
26
+
27
+ ## [0.60.0] - 2026-10-01
28
+
29
+ ### Fixed
30
+ - Preserve task/project attachment when the public assignment creation path archives its source configuration, so managed attempts retain immutable journal authority.
31
+ - Cache-free evidence lookup handles a seed-only journal ref without creating a checkout or treating an absent attempt as a corrupt journal.
32
+
33
+ ### Added
34
+ - `attempt evidence --kind review-approval` validates accepted independent approval separately from collection; retained collection/approval receipts support explicit historical-head validation without granting current check authority.
35
+ - Bind campaign check names to accepted executed operations and review collection to accepted `review-collect` artifacts, through `attempt evidence` purpose checks.
36
+
37
+ - `ace-assign attempt evidence` exposes read-only coordinator-accepted current-head check proof. Journal reads use immutable Git objects without writing cache/audit checkout state.
38
+ - Review receipts may consume a durable ace-review campaign result. The existing coordinator still validates authority, live head, independent reviewer, executed checks and artifact digests; stale or conflicting campaign snapshots are rejected.
39
+
40
+ ## [0.59.0] - 2026-09-29
41
+
42
+ ### Added
43
+ - Durable lifecycle exclusion (`Ace::Assign::Molecules::LifecycleExclusion`): prune holds the exclusive side from final evidence reads through removal while every supported start path -- attempt registration, fork/driver session launch, and worktree provisioning -- holds the shared side. Locks live outside every deletion target (sandbox-aware cache base or the shared Git common dir) so they survive removal of the target, and a `removed` marker makes post-prune starts fail closed instead of recreating a deleted identity.
44
+
45
+ ### Changed
46
+ - `AttemptCoordinator#start` and `ForkSessionLauncher#launch` now participate in the prune exclusion: a concurrent prune blocks new writers for the identity, and a start targeting a pruned assignment raises a conflict instead of writing into a recreated path.
47
+
10
48
  ## [0.58.0] - 2026-09-28
11
49
 
12
50
  ### Added
@@ -503,7 +541,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
503
541
  - Unified dynamic step default instructions under a single `AssignmentExecutor::DEFAULT_DYNAMIC_STEP_INSTRUCTIONS` source.
504
542
  - Limited canonical batch expansion to explicit `workflow`, `skill`, or declared `sub_steps` inputs, preserving prior numbering behavior for plain flat batch inserts.
505
543
  - Updated usage, getting-started, and handbook docs to cover the new `add --from` workflow.
506
- - Updated `record-demo` step instructions to require diagnosis before skipping on failure — agents must check config, available fonts, and spike findings before reporting a non-blocking skip.
544
+ - Updated `record-demo` step instructions to require diagnosis before skipping on failure -- agents must check config, available fonts, and spike findings before reporting a non-blocking skip.
507
545
  - Added `decision_notes.non_blocking_policy` to `record-demo` step catalog entry clarifying that "non-blocking" means "diagnose and retry before skipping," not "skip on first failure."
508
546
 
509
547
  ### Technical
@@ -959,7 +997,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
959
997
  ## [0.20.0] - 2026-03-05
960
998
 
961
999
  ### Added
962
- - Provider-specific session detection fallback in `ForkSessionLauncher` — when a provider doesn't return a native `session_id`, scans local session storage via `SessionFinder` to detect the forked session by prompt matching.
1000
+ - Provider-specific session detection fallback in `ForkSessionLauncher` -- when a provider doesn't return a native `session_id`, scans local session storage via `SessionFinder` to detect the forked session by prompt matching.
963
1001
  - Session metadata file (`<root>-session.yml`) written for every fork run, capturing `session_id`, `provider`, `model`, and `completed_at` for traceability.
964
1002
  - Stall error messages now include `Session: <id>` when session metadata is available, enabling direct trace to agent session.
965
1003
 
@@ -1044,14 +1082,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1044
1082
  ## [0.16.1] - 2026-03-04
1045
1083
 
1046
1084
  ### Fixed
1047
- - Correct `.agents/skills` typo to `.agent/skills` in default config and `SkillAssignSourceResolver` — skill discovery now uses the canonical provider-neutral path
1085
+ - Correct `.agents/skills` typo to `.agent/skills` in default config and `SkillAssignSourceResolver` -- skill discovery now uses the canonical provider-neutral path
1048
1086
 
1049
1087
  ## [0.16.0] - 2026-03-04
1050
1088
 
1051
1089
  ### Added
1052
- - New `onboard-base` catalog step — loads base project context via `ace-bundle project-base`
1053
- - New `task-load` catalog step — loads task behavioral spec via `ace-bundle task://<taskref>`
1054
- - Taskref placeholder substitution in catalog step descriptions — `<taskref>` in step descriptions is replaced with actual task reference during child instruction building
1090
+ - New `onboard-base` catalog step -- loads base project context via `ace-bundle project-base`
1091
+ - New `task-load` catalog step -- loads task behavioral spec via `ace-bundle task://<taskref>`
1092
+ - Taskref placeholder substitution in catalog step descriptions -- `<taskref>` in step descriptions is replaced with actual task reference during child instruction building
1055
1093
 
1056
1094
  ### Changed
1057
1095
  - Default assignment presets updated to use ace-task
@@ -1065,7 +1103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1065
1103
  ## [0.15.1] - 2026-03-01
1066
1104
 
1067
1105
  ### Added
1068
- - Fork-run crash recovery protocol in drive workflow — detection, commit partial work, progress report, inject recovery steps, re-fork pattern for partial completion scenarios
1106
+ - Fork-run crash recovery protocol in drive workflow -- detection, commit partial work, progress report, inject recovery steps, re-fork pattern for partial completion scenarios
1069
1107
 
1070
1108
  ## [0.15.0] - 2026-02-28
1071
1109
 
@@ -1222,7 +1260,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1222
1260
  ## [0.12.14] - 2026-02-22
1223
1261
 
1224
1262
  ### Added
1225
- - Subtree guard step in drive workflow — driver reviews all fork report files before continuing to next step
1263
+ - Subtree guard step in drive workflow -- driver reviews all fork report files before continuing to next step
1226
1264
  - Report review instruction in split-subtree-root step template for fork context
1227
1265
 
1228
1266
  ## [0.12.13] - 2026-02-22
@@ -1295,13 +1333,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1295
1333
  ## [0.12.1] - 2026-02-19
1296
1334
 
1297
1335
  ### Fixed
1298
- - `reflect-verify-cycle` pair changed from `sequential` to `conditional` — prevents overriding optional strength of reflect-and-refactor
1336
+ - `reflect-verify-cycle` pair changed from `sequential` to `conditional` -- prevents overriding optional strength of reflect-and-refactor
1299
1337
  - Renamed `max_recursion` to `max_reruns` in replan config for clarity
1300
1338
 
1301
1339
  ## [0.12.0] - 2026-02-19
1302
1340
 
1303
1341
  ### Added
1304
- - New `reflect-and-refactor` step in catalog — analyzes implementation against ATOM principles and executes targeted refactoring before PR creation
1342
+ - New `reflect-and-refactor` step in catalog -- analyzes implementation against ATOM principles and executes targeted refactoring before PR creation
1305
1343
  - Composition rules for reflect-and-refactor: ordering (after verify, before mark-done/release/retro), pairs (verify cycle, fix cycle, replan cycle), and conditional suggestion
1306
1344
  - `create-retro` step now consumes `findings-report` from reflect-and-refactor as recommended prerequisite
1307
1345
  - `implement-with-pr` recipe updated to include optional reflect-and-refactor step after work-on-task
@@ -1315,7 +1353,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1315
1353
 
1316
1354
  ### Fixed
1317
1355
 
1318
- - `assignment_state` now checks `completed` before `failed` — assignments where all steps are done/failed correctly report `:completed` instead of `:failed`
1356
+ - `assignment_state` now checks `completed` before `failed` -- assignments where all steps are done/failed correctly report `:completed` instead of `:failed`
1319
1357
 
1320
1358
  ### Added
1321
1359
 
@@ -1906,7 +1944,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1906
1944
 
1907
1945
  ### Fixed
1908
1946
 
1909
- - CLI `start` command crashes with positional argument (`ace-assign start job.yaml`) because `option :config` requires `--config` flag — renamed command to `create` with `argument :config`
1947
+ - CLI `start` command crashes with positional argument (`ace-assign start job.yaml`) because `option :config` requires `--config` flag -- renamed command to `create` with `argument :config`
1910
1948
  - `ace-bundle wfi://prepare-assignment` fails due to missing project-level wfi:// protocol registration
1911
1949
 
1912
1950
  ### Added
data/docs/usage.md CHANGED
@@ -107,6 +107,33 @@ ace-assign attempt reconcile --attempt ATTEMPT --receipt receipt.json
107
107
 
108
108
  `reserved -> running -> succeeded | failed | stopped | uncertain`; reconciliation may resolve `uncertain -> succeeded | failed`. Terminal attempts accept no new effects, and accepted history is append-only.
109
109
 
110
+ ### Read accepted execution authority
111
+
112
+ `attempt evidence` reads a managed coordinator-accepted receipt from immutable journal history without transitions, cache writes, or audit checkout creation. Supply the accepted attempt ID and exact canonical receipt digest. The trusted local operator or OS-enforced service verifies the actual executed outcome before accepting its receipt; worker-authored success claims cannot accept themselves. This reader consumes that attestation and does not execute a test or model.
113
+
114
+ ```sh
115
+ ace-assign attempt evidence --attempt ATTEMPT --receipt-digest SHA256 --format json \
116
+ --kind check --check-name tests
117
+ ace-assign attempt evidence --attempt ATTEMPT --receipt-digest SHA256 --format json \
118
+ --kind review-collection
119
+ ace-assign attempt evidence --attempt ATTEMPT --receipt-digest SHA256 --format json \
120
+ --kind review-collection --historical-head EXACT_RECORDED_HEAD
121
+ ace-assign attempt evidence --attempt ATTEMPT --receipt-digest SHA256 --format json \
122
+ --kind review-approval
123
+ ```
124
+
125
+ A check proof requires the live candidate head, intact accepted artifacts, and the matching executed operation: `tests` uses `test`, other explicit check names use that operation name. Review collection requires an accepted `review-collect` operation with a passed `review-execution` outcome and the retained metadata/report/prompt artifacts. Review approval requires an ordinary accepted `review` operation with the approved independent reviewer verdict, exact head and retained report artifacts; collection proof cannot substitute for approval. Both collection and approval support an exact `--historical-head`. Historical review validation exposes `historical: true` and the recorded head; it preserves accepted source authority across head drift and cannot certify live checks. Unknown receipts, changed artifacts, unaccepted/unmanaged history, wrong purposes and stale current queries fail closed. Retain the configured evidence ref alongside durable review campaigns.
126
+
127
+ ### Service effect claims
128
+
129
+ External service operations (driven through `ace-lab service request`) claim their effect on the assignment journal before dispatch, so a retried or crashed request can never duplicate a side effect. The coordinator owns the trusted surface; callers never append journal events directly.
130
+
131
+ - `Ace::Assign::Organisms::AttemptCoordinator#claim_service_request(binding)` records the claim and its attempt event atomically under a journal-wide request-ID index: the same request ID cannot be claimed twice, by another assignment, or against a stale candidate head. External-effect operations additionally require accepted review evidence at the current head, like `merge`.
132
+ - `#service_request_status(request_id)` reads the authoritative claim state, including after local cache loss.
133
+ - `#transition_service_request(request_id, state:, receipt:)` moves a claim to `succeeded` or `failed` only with a bound, non-secret receipt (exact request/attempt/project/operation/input digest/target fields, non-negative executor UID, and SHA-256 evidence references); rejected receipts never become approvals.
134
+ - `#reject_service_request(binding, reason:)` durably records an auditable rejection without inventing a valid attempt.
135
+ - Journal commits advance `journal_commit` only; they never change or authorize `candidate_head`, and `base_head` stays fixed at attempt start.
136
+
110
137
  ### Evidence storage and recovery modes
111
138
 
112
139
  - Task-attached (managed) attempts journal accepted evidence to the configured evidence Git ref (default `refs/ace/execution`) through an isolated, disposable audit checkout, outside the deliverable candidate branch. Journal commits never advance or exempt the reviewed candidate.
@@ -159,11 +186,14 @@ HITL stall behavior:
159
186
  - Canonical contract lives in `wfi://hitl` (`ace-hitl` package workflow).
160
187
  - If a step is failed with canonical message format `HITL: <id> <path>`, `ace-assign status` prints operator guidance with the matching `ace-hitl show <id>` command and available path hint.
161
188
  - Recommended resume flow:
189
+
162
190
  - `ace-hitl show <id>`
163
191
  - requester path (default): `ace-hitl wait <id>`
164
192
  - fallback path (when waiter inactive): `ace-hitl update <id> --answer "<decision>" --resume`
165
193
  - `ace-assign retry <failed-step> --assignment <assignment-id>`
194
+
166
195
  - Completion-attention flow:
196
+
167
197
  - When assignment work is complete but explicit user action is needed, create an approval HITL event (`kind=approval`) and include the resume instruction for `/as-assign-drive <assignment-id>`.
168
198
 
169
199
  ### `ace-assign step [STEP]`
@@ -267,6 +297,7 @@ Launch modes:
267
297
  - `headless`: force the existing provider subprocess path and never create tmux panes
268
298
  - `tmux`: require tmux context, create or reuse `<origin-window>-fs`, start a real interactive agent in a pane there via `ace-llm --interactive`, and send the scoped `/as-assign-drive <assignment>@<root>` handoff automatically. The fork window name uses the shared `ace-tmux` safe-name policy, so punctuation in the base window is replaced with `-`. Fork windows and panes are created detached, so the current tmux focus stays where the user left it.
269
299
  - `tmux`: require tmux context, create or reuse `<origin-window>-fs`, start a real interactive agent in a pane there via `ace-llm --interactive`, and send the scoped `/as-assign-drive <assignment>@<root>` handoff automatically
300
+
270
301
  - This mode consumes the shared `ace-tmux` runtime/control surface for tmux targeting, pane dispatch, and diagnostics.
271
302
  - The fork window name uses the shared `ace-tmux` safe-name policy, so punctuation in the base window is replaced with `-`.
272
303
  - Fork windows and panes are created detached, so the current tmux focus stays where the user left it.
@@ -338,6 +369,7 @@ When using preset-backed assignment creation (`ace-assign create --task ...`, `/
338
369
  - Terminal refs (`done`, `skipped`, `cancelled`) are skipped before queue expansion.
339
370
  - Mixed sets continue with remaining non-terminal refs and report skipped terminal refs.
340
371
  - If all requested refs are terminal, assignment creation stops with:
372
+
341
373
  - `All requested tasks are already terminal (done/skipped/cancelled): <refs>`
342
374
  - `No assignment created.`
343
375
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require_relative "base"
3
+ module Ace
4
+ module Assign
5
+ module CLI
6
+ module Commands
7
+ module Attempt
8
+ class Evidence < Ace::Support::Cli::Command
9
+ include Attempt::Base
10
+ desc "Read coordinator-accepted live execution or historical review authority"
11
+ option :attempt, required: true, desc: "Accepted execution attempt ID"
12
+ option :receipt_digest, required: true, desc: "Accepted receipt digest"
13
+ option :kind, default: "check", desc: "Evidence purpose: check, review-collection or review-approval"
14
+ option :check_name, default: "tests", desc: "Required check name (tests binds operation test)"
15
+ option :historical_head, desc: "Recorded collection/approval head for historical authority only"
16
+ option :format, default: "json", desc: "JSON evidence projection"
17
+
18
+ def call(**options)
19
+ raise Ace::Support::Cli::Error, "evidence supports --format json" unless options[:format] == "json"
20
+ emit_json(build_coordinator.evidence(attempt_id: options[:attempt], receipt_digest: options[:receipt_digest],
21
+ kind: options[:kind], check_name: options[:check_name], historical_head: options[:historical_head]))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -31,6 +31,7 @@ require_relative "atoms/evidence_digest"
31
31
  require_relative "atoms/assignment_scope"
32
32
 
33
33
  # Molecules
34
+ require_relative "molecules/lifecycle_exclusion"
34
35
  require_relative "molecules/assignment_manager"
35
36
  require_relative "molecules/attempt_store"
36
37
  require_relative "molecules/assignment_discoverer"
@@ -70,6 +71,7 @@ require_relative "cli/commands/attempt/start"
70
71
  require_relative "cli/commands/attempt/status"
71
72
  require_relative "cli/commands/attempt/finish"
72
73
  require_relative "cli/commands/attempt/reconcile"
74
+ require_relative "cli/commands/attempt/evidence"
73
75
 
74
76
  module Ace
75
77
  module Assign
@@ -94,6 +96,7 @@ module Ace
94
96
  ["fork-run", "Run subtree in forked process"],
95
97
  ["attempt start", "Start a scoped attempt for an assignment step"],
96
98
  ["attempt status", "Show attempt status for an assignment"],
99
+ ["attempt evidence", "Read accepted current-head check evidence"],
97
100
  ["attempt finish", "Finish an attempt with a structured execution receipt"],
98
101
  ["attempt reconcile", "Reconcile an interrupted or uncertain attempt"]
99
102
  ].freeze
@@ -157,6 +160,7 @@ module Ace
157
160
  register "fork-session", wrap_command(Commands::ForkSession)
158
161
  register "attempt start", wrap_command(Commands::Attempt::Start)
159
162
  register "attempt status", wrap_command(Commands::Attempt::Status)
163
+ register "attempt evidence", wrap_command(Commands::Attempt::Evidence)
160
164
  register "attempt finish", wrap_command(Commands::Attempt::Finish)
161
165
  register "attempt reconcile", wrap_command(Commands::Attempt::Reconcile)
162
166
 
@@ -17,6 +17,7 @@ module Ace
17
17
  TYPES = %w[
18
18
  intent process_start receipt_accepted transition
19
19
  candidate_invalidated reconciliation
20
+ service_claim service_transition
20
21
  ].freeze
21
22
 
22
23
  # Build an event chained to a previous digest.
@@ -24,7 +24,7 @@ module Ace
24
24
  ].freeze
25
25
 
26
26
  attr_reader :attempt_id, :assignment_id, :project_id, :scope, :operation,
27
- :producer, :head, :verdict, :artifacts, :checks, :review, :recorded_at, :digest
27
+ :producer, :head, :verdict, :artifacts, :checks, :review, :campaign, :recorded_at, :digest
28
28
 
29
29
  # @param attempt_id [String] Attempt the receipt belongs to
30
30
  # @param assignment_id [String] Owning assignment ID
@@ -40,7 +40,7 @@ module Ace
40
40
  # @param recorded_at [Time] Receipt creation time
41
41
  # @param digest [String, nil] Canonical digest (computed when nil)
42
42
  def initialize(attempt_id:, assignment_id:, project_id:, scope:, operation:, producer:, head:, verdict:,
43
- artifacts: [], checks: [], review: nil, recorded_at:, digest: nil)
43
+ artifacts: [], checks: [], review: nil, campaign: nil, recorded_at:, digest: nil)
44
44
  @attempt_id = attempt_id
45
45
  @assignment_id = assignment_id
46
46
  @project_id = project_id
@@ -52,6 +52,7 @@ module Ace
52
52
  @artifacts = artifacts
53
53
  @checks = checks
54
54
  @review = review
55
+ @campaign = campaign
55
56
  @recorded_at = recorded_at
56
57
  @digest = digest || Atoms::EvidenceDigest.digest(digest_payload)
57
58
  end
@@ -61,7 +62,7 @@ module Ace
61
62
  #
62
63
  # @return [Hash] Digest payload
63
64
  def digest_payload
64
- {
65
+ payload = {
65
66
  "attempt_id" => attempt_id,
66
67
  "assignment_id" => assignment_id,
67
68
  "project_id" => project_id,
@@ -74,6 +75,8 @@ module Ace
74
75
  "checks" => checks,
75
76
  "review" => review
76
77
  }
78
+ payload["campaign"] = campaign if campaign
79
+ payload
77
80
  end
78
81
 
79
82
  # Convert to a hash for serialization.
@@ -102,6 +105,7 @@ module Ace
102
105
  artifacts: data["artifacts"] || [],
103
106
  checks: data["checks"] || [],
104
107
  review: data["review"],
108
+ campaign: data["campaign"],
105
109
  recorded_at: parse_time(data["recorded_at"]) || Time.now.utc,
106
110
  digest: data["digest"]
107
111
  )
@@ -39,6 +39,24 @@ module Ace
39
39
  review_receipt = receipt_currency(receipts, "review", head)
40
40
  release_receipt = receipt_currency(receipts, "release", head)
41
41
  unresolved_effects = collect_unresolved_effects(attempts)
42
+ if assignment&.managed?
43
+ requests = journal.service_requests(assignment.id)
44
+ # A rejected effect stays unresolved when its claim had been
45
+ # dispatched (authorization consumed): only attributable evidence
46
+ # that no effect occurred settles it, never the bare rejection.
47
+ unresolved_effects.concat(requests
48
+ .select { |request| unresolved_service_state?(request) }
49
+ .map { |request| "#{request["request_id"]}:#{request["operation"]}:#{request["state"]}" })
50
+ # Settled requests keep their authorization only while their
51
+ # receipt evidence remains intact in the candidate repository:
52
+ # a deleted or modified artifact drops the outcome back to
53
+ # unresolved instead of trusting an unverifiable receipt.
54
+ unresolved_effects.concat(requests
55
+ .select { |request| request["state"] == "succeeded" || request["state"] == "failed-settled" }
56
+ .reject { |request| evidence_intact?(request) }
57
+ .map { |request| "#{request["request_id"]}:#{request["operation"]}:evidence-unavailable" })
58
+ unresolved_effects.uniq!
59
+ end
42
60
  feedback_state = derive_feedback_state(attempts, receipts, head, unresolved_effects)
43
61
 
44
62
  # Authorization requires managed evidence, settled work (no active
@@ -67,7 +85,7 @@ module Ace
67
85
  base_head: latest&.binding&.base_head,
68
86
  candidate_head: latest&.candidate_head,
69
87
  evidence_git_ref: evidence_git_ref(assignment),
70
- journal_commit: latest&.journal_commit,
88
+ journal_commit: assignment&.managed? ? journal.ref_value : latest&.journal_commit,
71
89
  unresolved_effects: unresolved_effects
72
90
  }
73
91
  evidence[:decision_digest] = Digest::SHA256.hexdigest(
@@ -78,6 +96,38 @@ module Ace
78
96
 
79
97
  private
80
98
 
99
+ # Accepted, uncertain, and failed effects are unresolved by
100
+ # definition; a rejected effect stays unresolved when its claim was
101
+ # dispatched (authorization consumed), because the effect may have
102
+ # happened.
103
+ def unresolved_service_state?(request)
104
+ %w[accepted uncertain failed].include?(request["state"]) ||
105
+ (request["state"] == "rejected" && request["consumed"] != false)
106
+ end
107
+
108
+ # Every receipt evidence artifact must still exist in the candidate
109
+ # repository with its recorded digest; evidence lives in the working
110
+ # tree, so its integrity is re-checked on every calculation.
111
+ def evidence_intact?(request)
112
+ receipt = request["receipt"]
113
+ return false unless receipt.is_a?(Hash)
114
+ repo_root = File.realpath(@repo_root)
115
+ Array(receipt["evidence"]).all? do |item|
116
+ path = File.expand_path(item["ref"].to_s, repo_root)
117
+ real = begin
118
+ File.realpath(path)
119
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
120
+ next false
121
+ end
122
+ begin
123
+ real.start_with?(repo_root + File::SEPARATOR) &&
124
+ Digest::SHA256.file(real).hexdigest == item["sha256"]
125
+ rescue Errno::EACCES, Errno::ELOOP
126
+ false
127
+ end
128
+ end
129
+ end
130
+
81
131
  def git_facts
82
132
  head, _s = Open3.capture2("git", "rev-parse", "HEAD", chdir: @repo_root, stdin_data: "")
83
133
  tree, _s = Open3.capture2("git", "rev-parse", "HEAD^{tree}", chdir: @repo_root, stdin_data: "")