ace-assign 0.57.1 → 0.60.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/config.yml +13 -0
  3. data/CHANGELOG.md +46 -12
  4. data/docs/exit-codes.md +10 -0
  5. data/docs/usage.md +49 -0
  6. data/handbook/workflow-instructions/assign/drive.wf.md +64 -8
  7. data/lib/ace/assign/atoms/assignment_scope.rb +53 -0
  8. data/lib/ace/assign/atoms/attempt_state_machine.rb +76 -0
  9. data/lib/ace/assign/atoms/evidence_digest.rb +78 -0
  10. data/lib/ace/assign/cli/commands/attempt/base.rb +35 -0
  11. data/lib/ace/assign/cli/commands/attempt/evidence.rb +28 -0
  12. data/lib/ace/assign/cli/commands/attempt/finish.rb +35 -0
  13. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +38 -0
  14. data/lib/ace/assign/cli/commands/attempt/start.rb +45 -0
  15. data/lib/ace/assign/cli/commands/attempt/status.rb +54 -0
  16. data/lib/ace/assign/cli/commands/status.rb +24 -1
  17. data/lib/ace/assign/cli.rb +34 -2
  18. data/lib/ace/assign/models/assignment.rb +21 -4
  19. data/lib/ace/assign/models/attempt.rb +178 -0
  20. data/lib/ace/assign/models/attempt_binding.rb +116 -0
  21. data/lib/ace/assign/models/evidence_event.rb +82 -0
  22. data/lib/ace/assign/models/execution_receipt.rb +139 -0
  23. data/lib/ace/assign/molecules/assignment_manager.rb +35 -3
  24. data/lib/ace/assign/molecules/attempt_reconciler.rb +88 -0
  25. data/lib/ace/assign/molecules/attempt_store.rb +201 -0
  26. data/lib/ace/assign/molecules/evidence_calculator.rb +144 -91
  27. data/lib/ace/assign/molecules/evidence_journal.rb +423 -0
  28. data/lib/ace/assign/molecules/execution_identity_resolver.rb +97 -0
  29. data/lib/ace/assign/molecules/fork_session_launcher.rb +38 -19
  30. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +209 -0
  31. data/lib/ace/assign/molecules/receipt_verifier.rb +272 -0
  32. data/lib/ace/assign/organisms/assignment_executor.rb +12 -4
  33. data/lib/ace/assign/organisms/attempt_coordinator.rb +618 -0
  34. data/lib/ace/assign/organisms/task_assignment_creator.rb +1 -1
  35. data/lib/ace/assign/version.rb +1 -1
  36. data/lib/ace/assign.rb +56 -0
  37. metadata +36 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64ca0ab83e21423599cccecfdc9b20e7dfc4c62b0e262e40bf82dc96ba68e0e0
4
- data.tar.gz: 5515ff2144574af8f820437a1423106925ec6f8674f5838921249445c8547f3b
3
+ metadata.gz: 3a8f387dd91c6da87ca5eb3712cb5fb71051a0326dfa13cf0d9d91e054c1cc00
4
+ data.tar.gz: 4698ad04a950a1401caec3387ef085e8c66512a07e9dd5af168446d80e7d89eb
5
5
  SHA512:
6
- metadata.gz: 42b4a978e165872d2bb9c89e6ab9cd6ddc6698c2664d054c0c6fde53de2b18981288dde5f6760e2f4ca2fe6b41811deeafb1435b9071b80a36fe211a2abeeb00
7
- data.tar.gz: f6c97237effc40614bcceb08b25a577a70fe0bc0d034791df6f90a8cf0050d9e9adf935bf89834bfd0a7641b2c3f5c5787bd8b73941e9784b04bcb479a0c61d9
6
+ metadata.gz: 89c8df3b2e0823c195dfc3d3b59fbcc58e79c97b3978e0db2e4395c83e86307e8d450a637e633b9aa947efeb79ff94ba78307bc24de577effb8cb55b53e9b8f5
7
+ data.tar.gz: dd2e0a73591ea2e646c9c8164cb56269ff6353860005acdfb57bc268ca8e2d5c89b77b3a19af8a1b895dcfa8716199ae0272a1705387dc4c5f5bda424c062668
@@ -28,6 +28,19 @@ execution:
28
28
  timeout: 1800
29
29
  launch_mode: auto # auto | headless | tmux
30
30
 
31
+ # Attempt evidence persistence (managed attempts)
32
+ attempt:
33
+ # Dedicated Git ref holding accepted execution evidence, outside the
34
+ # deliverable candidate branch; journal paths are execution/<assignment-id>/
35
+ evidence_git_ref: "refs/ace/execution"
36
+ # Disposable isolated audit checkout root (relative to project root)
37
+ evidence_checkout_root: ".ace-local/assign/evidence-checkout"
38
+ # Trusted execution-boundary identity adapter: local (interactive
39
+ # coordinator) or service (identity supplied by a trusted service executor
40
+ # via service_identity_env). Unknown identity fails closed.
41
+ identity_adapter: "local"
42
+ service_identity_env: "ACE_ASSIGN_SERVICE_IDENTITY"
43
+
31
44
  # Subtree execution behavior
32
45
  subtree:
33
46
  pre_commit_review: true
data/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.60.0] - 2026-10-01
11
+
12
+ ### Fixed
13
+ - Preserve task/project attachment when the public assignment creation path archives its source configuration, so managed attempts retain immutable journal authority.
14
+ - Cache-free evidence lookup handles a seed-only journal ref without creating a checkout or treating an absent attempt as a corrupt journal.
15
+
16
+ ### Added
17
+ - `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.
18
+ - Bind campaign check names to accepted executed operations and review collection to accepted `review-collect` artifacts, through `attempt evidence` purpose checks.
19
+
20
+ - `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.
21
+ - 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.
22
+
23
+ ## [0.59.0] - 2026-09-29
24
+
25
+ ### Added
26
+ - 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.
27
+
28
+ ### Changed
29
+ - `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.
30
+
31
+ ## [0.58.0] - 2026-09-28
32
+
33
+ ### Added
34
+ - Durable assignment attempts: `ace-assign attempt start|status|finish|reconcile` bind an immutable attempt ID to an assignment step/subtree, project, boundary-derived actor identity, and `base_head`, with a `reserved -> running -> succeeded/failed/stopped/uncertain` lifecycle and conservative reconciliation of interruptions (no automatic replay of merge/publish/deploy).
35
+ - Managed attempts journal accepted execution evidence to the configured evidence Git ref (default `refs/ace/execution`) through an isolated audit checkout with flock serialization and expected-old-value `git update-ref` compare-and-swap; `refs/ace/execution` never advances the deliverable candidate branch.
36
+ - Structured execution receipts: verifiable artifact digests, executed checks, executed independent reviewer verdicts for the exact current head, and rejection of credentials/terminal output in accepted evidence.
37
+ - `ace-assign status` (JSON) now includes the active attempt, `base_head`, `candidate_head`, `evidence_git_ref`, `journal_commit`, and unresolved effects.
38
+
39
+ ### Changed
40
+ - BREAKING: delivery evidence is derived exclusively from accepted, current-head journal receipts. The previous interpretation that a current `.ace-local/review`/`.ace-local/release` report plus hardcoded `terminal` feedback authorizes merge is removed; `merge_decision` is now `authorized` or `approval-required` (never `report-only`). Existing `EvidenceCalculator` consumers must read evidence from attempts.
41
+ - BREAKING: taskless assignments persist attempts locally (`recovery_mode: local_only`), never claim Git-backed recovery, and cannot record external effects until attached to a source task.
42
+ - New `attempt` error family exits with code 5 (conflict, rejected receipt, unauthorized identity, unavailable evidence, illegal attempt state).
43
+
10
44
  ## [0.57.1] - 2026-09-27
11
45
 
12
46
  ### Technical
@@ -490,7 +524,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
490
524
  - Unified dynamic step default instructions under a single `AssignmentExecutor::DEFAULT_DYNAMIC_STEP_INSTRUCTIONS` source.
491
525
  - Limited canonical batch expansion to explicit `workflow`, `skill`, or declared `sub_steps` inputs, preserving prior numbering behavior for plain flat batch inserts.
492
526
  - Updated usage, getting-started, and handbook docs to cover the new `add --from` workflow.
493
- - 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.
527
+ - 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.
494
528
  - 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."
495
529
 
496
530
  ### Technical
@@ -946,7 +980,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
946
980
  ## [0.20.0] - 2026-03-05
947
981
 
948
982
  ### Added
949
- - 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.
983
+ - 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.
950
984
  - Session metadata file (`<root>-session.yml`) written for every fork run, capturing `session_id`, `provider`, `model`, and `completed_at` for traceability.
951
985
  - Stall error messages now include `Session: <id>` when session metadata is available, enabling direct trace to agent session.
952
986
 
@@ -1031,14 +1065,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1031
1065
  ## [0.16.1] - 2026-03-04
1032
1066
 
1033
1067
  ### Fixed
1034
- - Correct `.agents/skills` typo to `.agent/skills` in default config and `SkillAssignSourceResolver` — skill discovery now uses the canonical provider-neutral path
1068
+ - Correct `.agents/skills` typo to `.agent/skills` in default config and `SkillAssignSourceResolver` -- skill discovery now uses the canonical provider-neutral path
1035
1069
 
1036
1070
  ## [0.16.0] - 2026-03-04
1037
1071
 
1038
1072
  ### Added
1039
- - New `onboard-base` catalog step — loads base project context via `ace-bundle project-base`
1040
- - New `task-load` catalog step — loads task behavioral spec via `ace-bundle task://<taskref>`
1041
- - Taskref placeholder substitution in catalog step descriptions — `<taskref>` in step descriptions is replaced with actual task reference during child instruction building
1073
+ - New `onboard-base` catalog step -- loads base project context via `ace-bundle project-base`
1074
+ - New `task-load` catalog step -- loads task behavioral spec via `ace-bundle task://<taskref>`
1075
+ - Taskref placeholder substitution in catalog step descriptions -- `<taskref>` in step descriptions is replaced with actual task reference during child instruction building
1042
1076
 
1043
1077
  ### Changed
1044
1078
  - Default assignment presets updated to use ace-task
@@ -1052,7 +1086,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1052
1086
  ## [0.15.1] - 2026-03-01
1053
1087
 
1054
1088
  ### Added
1055
- - Fork-run crash recovery protocol in drive workflow — detection, commit partial work, progress report, inject recovery steps, re-fork pattern for partial completion scenarios
1089
+ - Fork-run crash recovery protocol in drive workflow -- detection, commit partial work, progress report, inject recovery steps, re-fork pattern for partial completion scenarios
1056
1090
 
1057
1091
  ## [0.15.0] - 2026-02-28
1058
1092
 
@@ -1209,7 +1243,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1209
1243
  ## [0.12.14] - 2026-02-22
1210
1244
 
1211
1245
  ### Added
1212
- - Subtree guard step in drive workflow — driver reviews all fork report files before continuing to next step
1246
+ - Subtree guard step in drive workflow -- driver reviews all fork report files before continuing to next step
1213
1247
  - Report review instruction in split-subtree-root step template for fork context
1214
1248
 
1215
1249
  ## [0.12.13] - 2026-02-22
@@ -1282,13 +1316,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1282
1316
  ## [0.12.1] - 2026-02-19
1283
1317
 
1284
1318
  ### Fixed
1285
- - `reflect-verify-cycle` pair changed from `sequential` to `conditional` — prevents overriding optional strength of reflect-and-refactor
1319
+ - `reflect-verify-cycle` pair changed from `sequential` to `conditional` -- prevents overriding optional strength of reflect-and-refactor
1286
1320
  - Renamed `max_recursion` to `max_reruns` in replan config for clarity
1287
1321
 
1288
1322
  ## [0.12.0] - 2026-02-19
1289
1323
 
1290
1324
  ### Added
1291
- - New `reflect-and-refactor` step in catalog — analyzes implementation against ATOM principles and executes targeted refactoring before PR creation
1325
+ - New `reflect-and-refactor` step in catalog -- analyzes implementation against ATOM principles and executes targeted refactoring before PR creation
1292
1326
  - Composition rules for reflect-and-refactor: ordering (after verify, before mark-done/release/retro), pairs (verify cycle, fix cycle, replan cycle), and conditional suggestion
1293
1327
  - `create-retro` step now consumes `findings-report` from reflect-and-refactor as recommended prerequisite
1294
1328
  - `implement-with-pr` recipe updated to include optional reflect-and-refactor step after work-on-task
@@ -1302,7 +1336,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1302
1336
 
1303
1337
  ### Fixed
1304
1338
 
1305
- - `assignment_state` now checks `completed` before `failed` — assignments where all steps are done/failed correctly report `:completed` instead of `:failed`
1339
+ - `assignment_state` now checks `completed` before `failed` -- assignments where all steps are done/failed correctly report `:completed` instead of `:failed`
1306
1340
 
1307
1341
  ### Added
1308
1342
 
@@ -1893,7 +1927,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1893
1927
 
1894
1928
  ### Fixed
1895
1929
 
1896
- - 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`
1930
+ - 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`
1897
1931
  - `ace-bundle wfi://prepare-assignment` fails due to missing project-level wfi:// protocol registration
1898
1932
 
1899
1933
  ### Added
data/docs/exit-codes.md CHANGED
@@ -20,6 +20,7 @@ This document describes the exit codes returned by the `ace-assign` CLI.
20
20
  | 2 | Assignment error | No active assignment or assignment not found |
21
21
  | 3 | Configuration not found | Config file does not exist |
22
22
  | 4 | Step not found | Referencing a step that does not exist |
23
+ | 5 | Attempt error | Attempt conflict, rejected receipt, unauthorized identity, unavailable evidence, or illegal attempt state |
23
24
 
24
25
  ## Exit Code Details
25
26
 
@@ -55,6 +56,15 @@ The requested step reference does not exist in the target assignment:
55
56
  - `retry` with unknown step number
56
57
  - Commands that target a specific missing step
57
58
 
59
+ ### Exit Code 5: Attempt Error
60
+ An attempt, receipt, identity, or evidence problem rejected the operation:
61
+ - `attempt start` with a conflicting binding for an owned subtree
62
+ - `attempt finish`/`attempt reconcile` with a receipt that fails verification (wrong binding, stale head, digest mismatch, self-approval, forbidden fields)
63
+ - Worker-identity attempts to accept a succeeded verdict
64
+ - Unknown or missing execution-boundary identity
65
+ - Reconciling a terminal attempt, or resolving uncertainty without a receipt
66
+ - Unavailable or unwritable evidence storage blocking a managed effect
67
+
58
68
  ## See Also
59
69
 
60
70
  - [Usage Guide](usage.md) for full command reference
data/docs/usage.md CHANGED
@@ -85,6 +85,55 @@ ace-assign add --step review-pr --after 100 --child
85
85
  ace-assign add --yaml .ace-local/assign/jobs/add-task.yml --after 010 --child
86
86
  ```
87
87
 
88
+ ## Attempt Evidence
89
+
90
+ Execution evidence lives in assignment attempts, not in reports. An attempt binds an immutable ID to an assignment, step/subtree scope, project, boundary-derived actor/role/runtime, source task, and `base_head`; the deliverable `candidate_head` is pinned only when accepted evidence is submitted, and the evidence journal commit is tracked separately from both.
91
+
92
+ ### Attempt lifecycle
93
+
94
+ ```bash
95
+ ace-assign attempt start --assignment ASSIGNMENT --step STEP --project PROJECT
96
+ ace-assign attempt status --assignment ASSIGNMENT --format json
97
+ ace-assign attempt finish --attempt ATTEMPT --receipt receipt.json
98
+ ace-assign attempt reconcile --attempt ATTEMPT --receipt receipt.json
99
+ ```
100
+
101
+ - `attempt start` returns a JSON projection of the attempt (ID, state, binding facts, evidence references, digests). A repeated identical start returns the same attempt; a conflicting start exits 5 without launching a second writer.
102
+ - `attempt status` exposes only state, binding, references, and digests -- never credentials or receipt bytes.
103
+ - `attempt finish` accepts a structured receipt: operation, attributed producer, exact tested/reviewed head, verdict, artifact paths with SHA-256 digests, executed checks, and -- for review operations -- an executed independent reviewer verdict for that head.
104
+ - `attempt reconcile` classifies interrupted attempts (`stopped` before process start, `uncertain` when an effect cannot be proven either way, still `running` only for verifiably live processes) and resolves uncertainty only against a verified receipt attributed to the recorded execution boundary. Merge, publish, and deploy effects are never replayed automatically.
105
+
106
+ ### State machine
107
+
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
+
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
+ ### Evidence storage and recovery modes
128
+
129
+ - 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.
130
+ - Taskless assignments persist attempts under `.ace-local/assign` with `recovery_mode: local_only`. This path never claims Git-backed recovery and cannot record external effects (`merge`, `publish`, `deploy`, `release`); attach the assignment to a source task before managed delivery.
131
+ - Actor identity always comes from the execution boundary (OS login for local use, or the trusted service executor's identity). Unknown identity fails closed; flags never grant identity or authority.
132
+
133
+ ### status integration
134
+
135
+ `ace-assign status --format json` includes the active attempt, `base_head`, `candidate_head`, `evidence_git_ref`, `journal_commit`, and unresolved effects. Feedback state is derived from accepted current-head evidence: `terminal` (accepted evidence at the current head), `open` (attempts without a terminal outcome), `uncertain` (unresolved interruptions), or `unknown` (no attempts). Merge authorization requires an executed independent review verdict for the current head plus an independent release receipt when relevant; report files, exit codes, and prose never authorize a merge.
136
+
88
137
  ## Commands
89
138
 
90
139
  ### `ace-assign create`
@@ -657,21 +657,71 @@ Follow the instructions directly, performing the described work:
657
657
 
658
658
  ### 4. External Action Rule (Attempt-First)
659
659
 
660
- For external-facing steps (for example PR/review/release/push/update lifecycle steps):
660
+ Every external-facing step (implement, verify, review, merge, publish, deploy, release) runs inside a scoped attempt. The attempt -- not the report and not exit status -- is the delivery record.
661
661
 
662
- - Attempt the step command(s) first.
663
- - If blocked, capture concrete evidence:
662
+ **Step 1: Start the attempt before executing the step.** Actor identity is derived from the execution boundary; never pass actor flags.
664
663
 
665
- - command attempted
666
- - exact error output
667
- - why the step cannot proceed
664
+ ```bash
665
+ ATTEMPT_JSON=$(ace-assign attempt start --assignment "$ASSIGNMENT_ID" --step "$STEP_NUMBER" --project "$PROJECT_ID")
666
+ echo "$ATTEMPT_JSON"
667
+ ```
668
+
669
+ - A repeated identical start returns the same attempt; a conflicting start fails (exit 5). Never launch a second writer for an owned subtree.
670
+ - `base_head` is captured exactly once at start. Record the `attempt_id`.
671
+
672
+ **Step 2: Execute the step command(s).** If blocked, capture concrete evidence:
673
+
674
+ - command attempted
675
+ - exact error output
676
+ - why the step cannot proceed
668
677
 
669
- - Mark step failed with evidence (do not report synthetic completion).
678
+ Mark the step failed with evidence (do not report synthetic completion):
670
679
 
671
680
  ```bash
672
681
  ace-assign fail --message "Command failed: <cmd>. Error: <exact stderr>" --assignment "$ASSIGNMENT_TARGET"
673
682
  ```
674
683
 
684
+ **Step 3: Submit a structured receipt -- never prose -- to close the attempt.**
685
+
686
+ ```bash
687
+ cat > /tmp/attempt-receipt.json << 'EOF'
688
+ {
689
+ "attempt_id": "<ATTEMPT_ID>",
690
+ "assignment_id": "<ASSIGNMENT_ID>",
691
+ "project_id": "<PROJECT_ID>",
692
+ "scope": "<STEP_NUMBER>",
693
+ "operation": "implement",
694
+ "producer": {"actor": "<boundary-actor>", "role": "worker", "runtime": "<boundary-runtime>"},
695
+ "head": "<exact tested/reviewed HEAD sha>",
696
+ "verdict": "succeeded",
697
+ "artifacts": [{"path": "report.md", "sha256": "<sha256>"}],
698
+ "checks": [{"name": "ace-test", "verdict": "passed"}],
699
+ "review": {"reviewer": {"actor": "<independent-reviewer>", "runtime": "<reviewer-runtime>"}, "verdict": "approved", "head": "<same HEAD sha>"}
700
+ }
701
+ EOF
702
+
703
+ ace-assign attempt finish --attempt "$ATTEMPT_ID" --receipt /tmp/attempt-receipt.json
704
+ ```
705
+
706
+ Receipt rules enforced by the coordinator:
707
+
708
+ - `succeeded` verdicts require verifiable artifact digests, passing checks, and trusted coordinator/service authority; worker runtimes may submit attributable results but cannot self-approve.
709
+ - Review approval requires an executed independent reviewer verdict for the exact current head; same author/reviewer and report-only existence are rejected.
710
+ - Receipts carrying credentials, environment dumps, or terminal output are rejected outright.
711
+ - External effects (`merge`, `publish`, `deploy`, `release`) additionally require an accepted independent review receipt for the current head. Taskless assignments cannot record external effects.
712
+
713
+ **Step 4: Handle interruptions by classifying and reconciling -- never by assuming.**
714
+
715
+ ```bash
716
+ ace-assign attempt status --assignment "$ASSIGNMENT_ID" --format json
717
+ ace-assign attempt reconcile --attempt "$ATTEMPT_ID"
718
+ ace-assign attempt reconcile --attempt "$ATTEMPT_ID" --receipt /tmp/attempt-receipt.json
719
+ ```
720
+
721
+ - A lost receipt after a possible effect is `uncertain`; it resolves only against a verified receipt attributed to the recorded execution boundary.
722
+ - Running attempts stay running only while their recorded process is verifiably live; anything else becomes `stopped` or `uncertain`.
723
+ - Never replay `merge`, `publish`, or `deploy` automatically. Reconciliation records evidence; it never re-executes effects.
724
+
675
725
  ### Human-in-the-Loop (HITL) Stall Protocol
676
726
 
677
727
  Canonical source of truth: `wfi://hitl`.
@@ -717,7 +767,7 @@ For a blocked step:
717
767
 
718
768
  ### 5. Write Report (Only After Real Execution)
719
769
 
720
- After completing the step work, write a brief report summarizing what was accomplished:
770
+ After the attempt accepted a receipt (or the step was failed with evidence), write a brief report summarizing what was accomplished. The report advances the queue; the accepted attempt receipt carries the delivery evidence:
721
771
 
722
772
  ```bash
723
773
  # Write report content to a temp file
@@ -737,6 +787,12 @@ EOF
737
787
  ace-assign finish --message /tmp/step-report.md --assignment "$ASSIGNMENT_TARGET"
738
788
  ```
739
789
 
790
+ Prohibited:
791
+
792
+ - Advancing a step when its attempt has no accepted receipt (or recorded failure evidence).
793
+ - Treating a written report, exit 0, or prose as proof of completion. Check `ace-assign attempt status --assignment "$ASSIGNMENT_ID" --format json` for accepted evidence and unresolved effects.
794
+ - Reporting `uncertain` attempts as complete; reconcile them first.
795
+
740
796
  ### 6. Verify State Transition (Required)
741
797
 
742
798
  After each `ace-assign finish --message` or `ace-assign fail`, verify queue state:
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Atoms
6
+ # Pure validation and canonicalization of attempt scopes.
7
+ #
8
+ # An attempt scope is the step/subtree an attempt owns, expressed as a
9
+ # step number ("010", "010.01"). Scopes are canonicalized before they
10
+ # key ownership locks, journal paths, or binding comparisons so that
11
+ # cosmetic differences cannot create a second writer.
12
+ module AssignmentScope
13
+ SCOPE_PATTERN = /\A\d+(?:\.\d+)*\z/.freeze
14
+
15
+ # Canonicalize and validate a step/subtree scope.
16
+ #
17
+ # @param step [String] Step or subtree root number
18
+ # @return [String] Canonical scope
19
+ # @raise [Ace::Assign::AttemptErrors::InvalidScope] if blank or malformed
20
+ def self.canonicalize(step)
21
+ scope = step.to_s.strip
22
+ if scope.empty? || !scope.match?(SCOPE_PATTERN)
23
+ raise Ace::Assign::AttemptErrors::InvalidScope,
24
+ "Invalid attempt scope '#{step}': expected a step number such as 010 or 010.01"
25
+ end
26
+
27
+ scope
28
+ end
29
+
30
+ # Filesystem-safe token for an assignment/scope pair, used to key
31
+ # active-attempt ownership records.
32
+ #
33
+ # @param assignment_id [String] Assignment ID
34
+ # @param scope [String] Canonical scope
35
+ # @return [String] Ownership key token
36
+ def self.ownership_key(assignment_id, scope)
37
+ "#{assignment_id}-#{canonicalize(scope)}.json"
38
+ end
39
+
40
+ # Compare two scopes for binding identity.
41
+ #
42
+ # @param left [String] First scope
43
+ # @param right [String] Second scope
44
+ # @return [Boolean] True if both canonicalize to the same scope
45
+ def self.equal?(left, right)
46
+ canonicalize(left) == canonicalize(right)
47
+ rescue Ace::Assign::AttemptErrors::InvalidScope
48
+ false
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Atoms
6
+ # Pure state machine for assignment attempt lifecycles.
7
+ #
8
+ # Legal transitions:
9
+ # - reserved -> running
10
+ # - running -> succeeded | failed | stopped | uncertain
11
+ # - uncertain -> succeeded | failed (only via reconciliation)
12
+ #
13
+ # Terminal states are succeeded, failed, and stopped. Terminal attempts
14
+ # never accept new transitions, effects, or receipts; accepted history is
15
+ # immutable. A stop is never success, and an uncertain attempt must be
16
+ # resolved through explicit reconciliation, never automatic replay.
17
+ module AttemptStateMachine
18
+ STATES = %w[reserved running succeeded failed stopped uncertain].freeze
19
+ TERMINAL_STATES = %w[succeeded failed stopped].freeze
20
+ RESOLVED_STATES = %w[succeeded failed].freeze
21
+
22
+ TRANSITIONS = {
23
+ "reserved" => %w[running].freeze,
24
+ "running" => %w[succeeded failed stopped uncertain].freeze,
25
+ "uncertain" => RESOLVED_STATES.freeze
26
+ }.freeze
27
+
28
+ # @return [Array<String>] All known attempt states
29
+ def self.states
30
+ STATES
31
+ end
32
+
33
+ # @param state [String] State to inspect
34
+ # @return [Boolean] True if the state is terminal (no further transitions)
35
+ def self.terminal?(state)
36
+ TERMINAL_STATES.include?(state.to_s)
37
+ end
38
+
39
+ # @param state [String] State to inspect
40
+ # @return [Boolean] True if the state awaits reconciliation
41
+ def self.uncertain?(state)
42
+ state.to_s == "uncertain"
43
+ end
44
+
45
+ # Check whether a transition is legal without raising.
46
+ #
47
+ # @param from [String] Current state
48
+ # @param to [String] Requested next state
49
+ # @return [Boolean] True if the transition is legal
50
+ def self.can_transition?(from, to)
51
+ TRANSITIONS.fetch(from.to_s, []).include?(to.to_s)
52
+ end
53
+
54
+ # Return the legal next states for a state.
55
+ #
56
+ # @param from [String] Current state
57
+ # @return [Array<String>] Legal next states (empty for unknown/terminal)
58
+ def self.next_states(from)
59
+ TRANSITIONS.fetch(from.to_s, [])
60
+ end
61
+
62
+ # Validate a transition.
63
+ #
64
+ # @param from [String] Current state
65
+ # @param to [String] Requested next state
66
+ # @raise [Ace::Assign::AttemptErrors::InvalidTransition] if illegal
67
+ def self.transition!(from, to)
68
+ return to.to_s if can_transition?(from, to)
69
+
70
+ raise Ace::Assign::AttemptErrors::InvalidTransition,
71
+ "Illegal attempt transition #{from} -> #{to} (legal from #{from}: #{next_states(from).join(', ')})"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+
6
+ module Ace
7
+ module Assign
8
+ module Atoms
9
+ # Pure canonical digest helpers for attempt evidence.
10
+ #
11
+ # Every accepted evidence record (events, receipts, artifacts) carries a
12
+ # SHA-256 digest over a canonical JSON serialization (sorted keys, no
13
+ # insignificant whitespace) so records remain comparable across writers
14
+ # and reloads. Digests are over non-secret metadata only.
15
+ module EvidenceDigest
16
+ # Canonical JSON serialization of an object.
17
+ #
18
+ # Sorts hash keys recursively and strips insignificant whitespace so
19
+ # semantically equal payloads produce byte-identical output.
20
+ #
21
+ # @param object [Object] JSON-serializable object
22
+ # @return [String] Canonical JSON string
23
+ def self.canonical_json(object)
24
+ JSON.generate(sort_keys(object))
25
+ end
26
+
27
+ # SHA-256 hex digest over the canonical JSON of an object.
28
+ #
29
+ # @param object [Object] JSON-serializable object
30
+ # @return [String] 64-character hex digest
31
+ def self.digest(object)
32
+ digest_string(canonical_json(object))
33
+ end
34
+
35
+ # SHA-256 hex digest over raw bytes.
36
+ #
37
+ # @param bytes [String] Raw byte content
38
+ # @return [String] 64-character hex digest
39
+ def self.digest_string(bytes)
40
+ Digest::SHA256.hexdigest(bytes)
41
+ end
42
+
43
+ # SHA-256 hex digest of a file's contents.
44
+ #
45
+ # @param path [String] File path
46
+ # @return [String] 64-character hex digest
47
+ # @raise [Errno::ENOENT] if the file does not exist
48
+ def self.digest_file(path)
49
+ digest_string(File.read(path))
50
+ end
51
+
52
+ # Short digest prefix for file naming (collision-resistant enough for
53
+ # append-only event filenames; full digests remain in the payload).
54
+ #
55
+ # @param object [Object] JSON-serializable object
56
+ # @return [String] 12-character hex prefix
57
+ def self.short_digest(object)
58
+ digest(object)[0, 12]
59
+ end
60
+
61
+ class << self
62
+ private
63
+
64
+ def sort_keys(value)
65
+ case value
66
+ when Hash
67
+ value.sort.to_h.transform_values { |v| sort_keys(v) }
68
+ when Array
69
+ value.map { |item| sort_keys(item) }
70
+ else
71
+ value
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Assign
7
+ module CLI
8
+ module Commands
9
+ # Public `ace-assign attempt` command namespace (interface contract:
10
+ # start, status, finish, reconcile).
11
+ module Attempt
12
+ # Shared coordinator construction, option validation, and JSON output.
13
+ module Base
14
+ private
15
+
16
+ def build_coordinator
17
+ Organisms::AttemptCoordinator.new
18
+ end
19
+
20
+ def require_option(options, key, usage)
21
+ value = options[key].to_s.strip
22
+ raise Ace::Support::Cli::Error, "Missing --#{key}: usage: ace-assign attempt #{usage}" if value.empty?
23
+
24
+ value
25
+ end
26
+
27
+ def emit_json(payload)
28
+ puts JSON.generate(payload)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "base"
5
+
6
+ module Ace
7
+ module Assign
8
+ module CLI
9
+ module Commands
10
+ module Attempt
11
+ # Accept a structured execution receipt and record the outcome.
12
+ # Terminal and uncertain attempts refuse new receipts.
13
+ class Finish < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+ include Attempt::Base
16
+
17
+ desc "Finish an attempt with a structured execution receipt"
18
+
19
+ option :attempt, desc: "Attempt ID"
20
+ option :receipt, desc: "Path to the receipt JSON file"
21
+
22
+ def call(**options)
23
+ attempt_id = require_option(options, :attempt, "finish --attempt ID --receipt FILE")
24
+ receipt = require_option(options, :receipt, "finish --attempt ID --receipt FILE")
25
+
26
+ result = build_coordinator.finish(attempt_id: attempt_id, receipt_path: receipt)
27
+
28
+ emit_json(result.projection)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end