ace-hitl 0.11.1 → 0.13.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +21 -27
  4. data/docs/usage.md +212 -63
  5. data/lib/ace/hitl/cli/commands/ask.rb +5 -27
  6. data/lib/ace/hitl/cli/commands/pending.rb +2 -1
  7. data/lib/ace/hitl/cli/commands/proposal.rb +75 -0
  8. data/lib/ace/hitl/cli/commands/serve.rb +1 -4
  9. data/lib/ace/hitl/cli/commands/update.rb +2 -0
  10. data/lib/ace/hitl/cli/commands/wait.rb +23 -14
  11. data/lib/ace/hitl/cli.rb +4 -1
  12. data/lib/ace/hitl/lifecycle/binding.rb +9 -11
  13. data/lib/ace/hitl/lifecycle/client.rb +106 -15
  14. data/lib/ace/hitl/lifecycle/duty.rb +2 -3
  15. data/lib/ace/hitl/lifecycle/effects.rb +4 -2
  16. data/lib/ace/hitl/lifecycle/kinds.rb +8 -11
  17. data/lib/ace/hitl/lifecycle/peer.rb +5 -4
  18. data/lib/ace/hitl/lifecycle/policy.rb +21 -7
  19. data/lib/ace/hitl/lifecycle/proposals.rb +285 -0
  20. data/lib/ace/hitl/lifecycle/protocol.rb +4 -3
  21. data/lib/ace/hitl/lifecycle/service.rb +64 -11
  22. data/lib/ace/hitl/lifecycle/service_publication_binding.rb +97 -0
  23. data/lib/ace/hitl/lifecycle/store.rb +259 -75
  24. data/lib/ace/hitl/live_client.rb +126 -0
  25. data/lib/ace/hitl/organisms/hitl_manager.rb +10 -51
  26. data/lib/ace/hitl/proposals/evaluator.rb +29 -0
  27. data/lib/ace/hitl/proposals/policy.rb +125 -0
  28. data/lib/ace/hitl/providers/lab/assignment_binding.rb +28 -3
  29. data/lib/ace/hitl/providers/lab/protected_assignment_binding.rb +75 -0
  30. data/lib/ace/hitl/providers/lab.rb +34 -26
  31. data/lib/ace/hitl/version.rb +1 -1
  32. data/lib/ace/hitl.rb +1 -1
  33. metadata +27 -9
  34. data/lib/ace/hitl/molecules/lab_projection_observer.rb +0 -97
  35. data/lib/ace/hitl/providers/lab/composite_binding.rb +0 -47
  36. data/lib/ace/hitl/providers/lab/daemon_binding.rb +0 -210
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2529e6b24197617c3554da5d1ff7348b19681769562259b05f6d88f469527cf1
4
- data.tar.gz: 76b205cac4da0c4d7a34f391ebf569b8c997af87d47edf8b607d602727f82582
3
+ metadata.gz: 2e0e3e52b0322d7e5b585bf30728ebaae32337e06b4a8be12e96dcba1c0d498b
4
+ data.tar.gz: 4b79e7e2d5efd486c794be95cf515d147b17f322227d3cd6d6583382f3e6fb7e
5
5
  SHA512:
6
- metadata.gz: 977f6398c3f1e6a10e198c267c9b65af6f6d3ff0a2e668c3e80e4a0ad79a0fc332b9f03081fd0f4bfcd7f995a8c6b3458d5afc1bb39063dafaa4516f2b51d04c
7
- data.tar.gz: be81c083da2b5af0a5c97646b621ec2b71a6950025e0da4a608c25a2d28142ec67bba8dab032ccf06c004c3021d1e3d660a2c384a58f4925f4b4690895ea53cb
6
+ metadata.gz: a7abb0c60530c467efbb7a2b99d514e23a716bd61fce8d67c65966d629bb70969785043d63dd36e60eabd1d708d7d850b58eca6b31e5135c071e2791147c3bd7
7
+ data.tar.gz: 339b0dee9be68ff3ef82b4de954e6642a58b4ccdf8cad109524b66991423f39f543a59f75e8e957c1be23e263975a3448009a3558aa3e622415059d14ad036bd
data/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.13.0] - 2026-10-08
11
+
12
+ ### Added
13
+
14
+ - Bind scoped publication to the original canonical HITL challenge and authorization; keep OTP material out of agent messages and ordinary transport.
15
+
16
+ ### Changed
17
+
18
+ - Read delivery history from the original Inbox owner. Complete human attention on terminal submission without requiring an agent-read receipt or querying recovery caches.
19
+
20
+ ### Fixed
21
+
22
+ - Route proposals through the retained project journal and preserve exact protected attempt identity across lifecycle calls.
23
+ - Exercise concurrent creation under the actual lifecycle lock.
24
+
25
+ ### Fixed
26
+
27
+ - Accept exact protected launch attempt identifiers for HITL requests while preserving assignment syntax and authoritative binding checks.
28
+
29
+ ## [0.12.0] - 2026-10-05
30
+
31
+ ### Added
32
+
33
+ - Add an opt-in installed proposal scenario with an isolated local gem closure, real Unix and tmux owner binding, controlled sixteen-hour restart, technical refusal, and one verified service effect/receipt.
34
+ - Resolve immutable second-commander proposals through confirmed-delivery sixteen-hour policy and canonical Assign authorization.
35
+
36
+ ### Changed
37
+
38
+ - Declare the required direct dependencies and minimum producer versions for this coordinated release: `ace-assign ~> 0.64`, `ace-herdr ~> 0.4`, `ace-hitl-contract ~> 0.2`.
39
+
40
+ - Replace daemon/Work bindings with the managed assignment envelope and kernel-attributed exact native reverse owner. Add explicit in-process delivery/watch, authenticated pane-less wait, visible pending recovery, and existing signed Inbox reconciliation; keep native transport and business effect receipts separate.
41
+
42
+ ### Fixed
43
+
44
+ - Recover revision operations by explicit source revision and stable operation identity without resetting delivery; require current project authority for proposal reads, refuse generic proposal creation, and preserve ordinary question character limits.
45
+
46
+ - Recover stable-ID proposal creation from canonical prepared requests, retain exact reply deduplication across retry ordering, and queue proposer reconciliation wakes for the existing transport actor.
47
+ - Bound pending IPC pages by encoded frame size while retaining all native recovery claims and project authorization; Ruby callers traverse keyset pages.
48
+ - Preserve the accepted native incarnation through answer consumption and require explicit signed-supersession retry before another submission.
49
+
10
50
  ## [0.11.1] - 2026-10-04
11
51
 
12
52
  ### Fixed
data/README.md CHANGED
@@ -10,7 +10,7 @@ Canonical workflow and skill for agents:
10
10
  ## Commands
11
11
 
12
12
  - `ace-hitl create` creates a HITL event
13
- - `ace-hitl ask` asks a human via HITL and forwards the request through a provider adapter (`--provider`, default `lab`); ONE operation: local event + relay request through the authenticated boundary (`--assignment/--attempt/--project` managed binding, `--work` legacy, effect callback flags)
13
+ - `ace-hitl ask` asks a human via HITL and forwards the request through a provider adapter (`--provider`, default `lab`); ONE operation: local event + relay request through the authenticated boundary (`--assignment/--attempt/--project` managed binding, effect callback flags)
14
14
  - `ace-hitl list` lists HITL events with filters (`--scope current|all`, all statuses by default)
15
15
  - `ace-hitl show` renders event details, path, or raw content (`--scope current|all`)
16
16
  - `ace-hitl update` updates frontmatter, answer content, and folder location
@@ -37,29 +37,23 @@ Use `ace-overseer status` for a global worktree dashboard.
37
37
 
38
38
  ## Provider adapters
39
39
 
40
- `ace-hitl ask` dispatches through the `Ace::Hitl::Providers` registry
41
- (selection: `--provider` flag → `ACE_HITL_PROVIDER` env → `lab`).
42
-
43
- - `ask` is ONE operation: it creates the local HITL event and the relay
44
- request through the NATIVE generic lifecycle store
45
- (`Ace::Hitl::Lifecycle`; migration spec 8wm.t.y21), then persists
46
- `provider`, `ref_schema`, `ref_session`, `ref_pane` plus the existing
47
- `lab_request_*` fields.
48
- - The asker's reverse address (`ref`, versioned schema
49
- `ace.hitl.ref/v1`: herdr session + pane) is captured fail-closed from
50
- `HERDR_SESSION` / `HERDR_PANE`; absent or invalid values abort the ask
51
- before any event is created or store state changes.
52
- - Error model: `UnknownProviderError`, `InvalidRefError`,
53
- `ProviderUnavailableError` (store-create failure; surfaces the orphan
54
- event id when one was already created), `UnsupportedOperationError`.
55
- - `deliver(ref, answer)` (push the answer back to the asker's pane) is
56
- declared by the interface; provider `lab` raises
57
- `UnsupportedOperationError` until the ace-herdr push-delivery
58
- integration lands. `ace-hitl wait` stays the pane-less script path and
59
- does not go through a provider.
60
- - The generic lifecycle is provider-agnostic; all lab coupling lives in
61
- the provider=lab seams (the `Providers::Lab::DaemonBinding` labd
62
- binding client and the store factory), enforced by guard tests.
40
+ `ace-hitl ask --question ... --assignment ... --attempt ...` creates a scoped
41
+ request through authenticated IPC. The managed coordinator verifies the exact
42
+ active native owner and reverse address; caller environment variables cannot
43
+ supply authority. No Lab daemon or Work binding is used.
44
+
45
+ `Ace::Hitl::LiveClient` provides explicit in-process `deliver`, `watch`, `wait`,
46
+ `status`, `pending`, and signed `reconcile`. `watch` returns a thread owned by
47
+ its calling agent. Native delivery commits an incarnation-bound event through
48
+ Herdr Inbox and registers it through Assign. Queue acceptance and wake do not
49
+ prove consumption. Only an event-bound signed native observation verified by
50
+ Herdr and journaled by Assign completes delivery. Business callbacks have
51
+ separate authorization and receipts and are never rerun by native retries.
52
+
53
+ Pane-less `wait --request ID` consumes an existing authorized request over IPC;
54
+ it neither creates a native target nor proves a business effect. OTP answers
55
+ use that protected path with their authorized operation; no OTP bytes or OTP
56
+ hash enter the shared envelope, folder or native inbox.
63
57
 
64
58
  ## Examples
65
59
 
@@ -67,8 +61,8 @@ Use `ace-overseer status` for a global worktree dashboard.
67
61
  ace-hitl list
68
62
  ace-hitl list --scope all
69
63
  ace-hitl create "Which auth strategy?" --kind decision --question "JWT or sessions?"
70
- ace-hitl ask "Proceed with deploy?" --work W685 --effect-arg /bin/false --effect-cwd /tmp
71
- ace-hitl ask "Proceed with deploy?" --provider lab --work W685
64
+ ace-hitl ask --question "Proceed with deploy?" --assignment assign685 --attempt attempt685 --effect-arg /bin/false --effect-cwd /tmp
65
+ ace-hitl ask --question "Proceed with deploy?" --provider lab --assignment assign685 --attempt attempt685
72
66
  ace-hitl show abc123 --content
73
67
  ace-hitl show abc123 --scope current
74
68
  ace-hitl update abc123 --answer "Use JWT with server-side refresh tokens."
@@ -78,7 +72,7 @@ ace-hitl update abc123 --answer "Use JWT with server-side refresh tokens." --res
78
72
 
79
73
  ## Testing
80
74
 
81
- This package is **fast-only** in the ACE testing model.
75
+ This package has deterministic fast and scoped integration tests.
82
76
 
83
77
  - Deterministic test coverage lives under `test/fast/`.
84
78
  - This migration does not introduce `test/feat/` or `test/e2e/` for this package.
data/docs/usage.md CHANGED
@@ -19,7 +19,7 @@ Runtime store default: `.ace-local/hitl/` (legacy `.ace-hitl/` is no longer used
19
19
 
20
20
  ## Testing
21
21
 
22
- `ace-hitl` is currently a **fast-only** package in the ACE testing model.
22
+ `ace-hitl` includes isolated fast tests and deterministic scoped integration tests.
23
23
 
24
24
  - Deterministic coverage lives under `test/fast/`.
25
25
  - This package does not introduce `test/feat/` or `test/e2e/` in this migration.
@@ -102,47 +102,52 @@ ace-hitl update abc123 --move-to next
102
102
  ace-hitl update abc123 --answer "close the assignment" --resume
103
103
  ```
104
104
 
105
- ## Ask (Provider adapter with effect callback)
106
-
107
- `ace-hitl ask` dispatches through the provider adapter registry
108
- (`--provider`, default: `ACE_HITL_PROVIDER` env, then `lab`). ONE
109
- operation: it creates the local HITL event, forwards the question through
110
- the provider transport bound to the event via `--ace-hitl-id`, and prints
111
- both ids. Effect declarations are validated client-side (exact bounds)
112
- and passed through verbatim into the native relay request store.
105
+ ## Ask and scoped live client
113
106
 
114
107
  ```bash
115
- ace-hitl ask "Proceed with deploy?" \
116
- --work W685 \
117
- --effect-arg /usr/bin/notify-send "{answer}" \
118
- --effect-cwd /tmp
108
+ ace-hitl ask --question "Proceed with deploy?" \
109
+ --assignment assign685 --attempt attempt685 --project ace \
110
+ --effect-arg /usr/bin/notify-send --effect-arg "{answer}" --effect-cwd /tmp
111
+ ```
112
+
113
+ Assignment and attempt are required compact managed IDs. The coordinator verifies
114
+ an active owner and its exact native reverse binding, using the kernel peer PID
115
+ and Runtime's existing process ancestry/birth authority. Missing peer PID or
116
+ native evidence refuses an exact-owner claim. Darwin uses LOCAL_PEERPID; Linux
117
+ uses SO_PEERCRED. UID equality alone never selects another attempt. There is no
118
+ Work binding, Lab daemon socket or environment-derived reverse target.
119
+
120
+ An agent explicitly hosts its watcher in its own process:
121
+
122
+ ```ruby
123
+ client = Ace::Hitl::LiveClient.new(root: checkout_root)
124
+ watcher = client.watch(request: request_id) { |delivery| handle_queue_result(delivery) }
125
+ watcher.value
126
+ client.status(request: request_id)
119
127
  ```
120
128
 
121
- - `--attempt` defaults to `LAB_ATTEMPT_ID`; `--project` to `ace`;
122
- `--harness` to `lab-admin`; `--plan` to `ace-hitl ask`.
123
- - Reverse address (fail closed): the asker's herdr session + pane are
124
- read from `HERDR_SESSION` / `HERDR_PANE` and persisted on the event as
125
- `ref_session` / `ref_pane` with `ref_schema: ace.hitl.ref/v1` and
126
- `provider: lab`. Absent or invalid values abort the ask before any
127
- event is created or transport is called — an ask must always know
128
- where its answer can be delivered.
129
- - Effect flags: `--effect-match` (regex, <= 200 chars, must compile),
130
- `--effect-arg` (repeatable, 1..16 x 1..512 chars after the lab's
131
- strip-then-bounds check; whitespace-only elements fail fast, valid
132
- values pass through verbatim; `{answer}` substituted lab-side),
133
- `--effect-cwd` (absolute, must exist), `--effect-timeout-s` (1..600).
134
- - Whether an effect was declared is recorded on the event as
135
- `lab_request_effect: declared|none` so `wait` can apply the right
136
- terminal semantics.
137
- - The answer is always relayed unchanged; consumption stays on the
138
- operator side via `ace-hitl consume`.
139
- - If the transport send fails after the local event was created, the
140
- error surfaces the event id as an orphan (created but never bound to a
141
- relay request); inspect it with `ace-hitl show <id>` and delete or
142
- re-ask as needed.
143
- - `deliver(ref, answer)` — pushing the answer back to the asker's pane —
144
- is declared by the adapter interface; provider `lab` reports it as
145
- unsupported until the ace-herdr push-delivery integration lands.
129
+ `deliver` consumes an authorized ordinary answer, enqueues one incarnation-bound
130
+ Herdr event, registers its original assignment/attempt attribution with Assign,
131
+ and sends to the exact selected terminal. The accepted owner's terminal, agent
132
+ and native session ID are checked against the live Herdr pane before sending.
133
+ A replaced agent in the same pane refuses rather than selecting another target.
134
+ Repeated calls reuse the same event; uncertain sends are not automatically
135
+ repeated. The configured Hermes transport owns Telegram request/reply routing.
136
+
137
+ A delivered acknowledgement means submitted to the terminal, not read by the
138
+ agent. LiveClient.status reads delivery history from the original Inbox owner;
139
+ delivered answers no longer appear as pending human attention. Current agent
140
+ progress/output comes from direct Herdr/tmux panel capture when needed.
141
+
142
+ Business effects still run through their scoped owner with their own result
143
+ and authorization. Neither a sent message nor panel output replaces verification
144
+ of those effects. No dedicated Codex app-server, message-read signer/key or
145
+ signed consumption/supersession reconciliation API is required.
146
+
147
+ The shared versioned envelope is semantically owned by HITL and packaged in
148
+ `ace-hitl-contract` to preserve the acyclic HITL → Assign → Herdr → contract
149
+ graph. Its nested Hermes message and reverse reference are distinct schemas.
150
+ OTP answers and their hashes are excluded from the envelope and native delivery.
146
151
 
147
152
  ## Scoped Store Boundary (spec 8wq.t.34i)
148
153
 
@@ -204,7 +209,7 @@ root is `ACE_HITL_OVERSEER_CHANNEL_ROOT` (default
204
209
  Managed binding (the default authority):
205
210
 
206
211
  ```bash
207
- ace-hitl ask "Choose the next scope" \
212
+ ace-hitl ask --question "Choose the next scope" \
208
213
  --assignment 8x3abc --attempt a1b2c3 --project ace
209
214
  ```
210
215
 
@@ -213,11 +218,8 @@ identity, verified through the ace-assign coordinator under the
213
218
  assignment exclusion: a stale, ended, replaced, or uncertain attempt
214
219
  cannot acquire authority, and the exclusion is HELD across every
215
220
  consume/deliver transition so an attempt cannot end between the
216
- liveness check and the commit. `--work W... --attempt A-...` remains
217
- the legacy binding until the provider=lab integration (vs2) switches
218
- consumers; the two authorities are mutually exclusive and
219
- `--attempt` is always required (environment variables are never
220
- attempt identity).
221
+ liveness check and the commit. Assignment and attempt identity are always explicit;
222
+ there is no legacy Work-only contract.
221
223
 
222
224
  Transport side:
223
225
 
@@ -285,25 +287,24 @@ ace-hitl wait abc123 --poll-every 600 --timeout 14400
285
287
  ace-hitl wait abc123 --scope current
286
288
  ```
287
289
 
288
- When the event carries a Lab request (`lab_request_id`), wait also observes
289
- the Lab public projection instead of hanging blind. Both projection fields
290
- are observed: the lifecycle `state` (created / answer-delivered / consumed /
291
- cancelled) and the separate `effect_state` (callback-pending-with-answer /
292
- callback-ok / callback-escalated). Terminal semantics are effect-aware:
293
-
294
- - Requests without a declared effect terminate on lifecycle states
295
- (`answer-delivered`, `consumed`, `cancelled`).
296
- - Effect-declaring requests keep waiting until the callback verdict
297
- (`callback-ok` or `callback-escalated`) appears — they never end
298
- silently at answer delivery; `callback-escalated` output points at
299
- the lab duty projection for the escalation.
300
- - The event's `lab_request_state` records the effective state, so it
301
- never claims plain `answer-delivered` while an effect outcome exists.
302
-
303
- Relay consumption stays on the operator side via `ace-hitl consume`.
304
- `wait` is the pane-less script path: agents with a herdr pane ask
305
- through the provider adapter and receive answers delivered back to
306
- their pane.
290
+ Managed event waits resolve `lab_request_id` and consume through authenticated
291
+ IPC. They never read folder projections or persist returned answers into the
292
+ local event. Explicit pane-less scripts can wait without any local event:
293
+
294
+ ```bash
295
+ ace-hitl wait --request hitl001 --timeout 30
296
+ ace-hitl wait --request otp001 --operation gem-push
297
+ ```
298
+
299
+ The default managed wait is indefinite; a positive timeout bounds only this
300
+ call. It never expires the request. OTP consumption requires the authorized
301
+ operation and returns its bytes only over protected IPC. Local event polling
302
+ remains available for ordinary unbound local events. Managed events cannot use
303
+ `update --resume` to launch unscoped session/shell delivery.
304
+
305
+ Installed acceptance uses actual registered Telegram ingress, the selected
306
+ live Herdr/tmux terminal and the real scoped users in Lab. Controlled local
307
+ transport tests do not replace launching and observing the actual task.
307
308
 
308
309
  ## Lifecycle Event Names
309
310
 
@@ -317,3 +318,151 @@ Canonical namespace for HITL lifecycle signaling:
317
318
  - `hitl.event.resume_skipped_waiter_active`
318
319
  - `hitl.event.resume_failed`
319
320
  - `hitl.event.archived`
321
+
322
+ ## Pending recovery history across bounded IPC pages
323
+
324
+ `ace-hitl pending --project ace` continues to show unresolved native delivery
325
+ claims even after their answers have been consumed. A large retained history
326
+ must not prevent newly created questions from reaching Telegram.
327
+
328
+ The lifecycle wire `pending` operation returns `{items, next}`. `next` is an
329
+ exclusive request-ID cursor; send it as `after` with the same project for the
330
+ next page. Each response, including framing, fits the IPC byte limit. Every
331
+ page rechecks transport/project authorization. `Lifecycle::Client#pending`
332
+ collects the pages for existing Ruby/CLI callers; `pending_page(project:, after:)`
333
+ exposes a single bounded page. `read(id)` remains available for exact recovery.
334
+ No consumed claim is deleted to make the list fit. A record too large for one
335
+ page produces a classified error, never a successful truncated list.
336
+
337
+ This is a live keyset scan, not a frozen snapshot: a newly inserted ID before
338
+ the current cursor appears on the next scan. Repeated polling therefore remains
339
+ required; a cursor is not proof of delivery or native consumption.
340
+
341
+ ## Immutable second-commander proposals
342
+
343
+ `ace-hitl proposal create proposal-0123456789abcdef01234567 --assignment ID --attempt ID --project ID --file proposal.json`
344
+ returns persisted immutable proposal/revision/request IDs and current awaiting-delivery state immediately, even with unavailable transport. delivered_at and deadline are absent until acknowledged submission; show exposes them afterward. The sole Hermes
345
+ polling actor publishes the full precise proposal and records confirmed submission before
346
+ HITL persists delivered_at and a deadline exactly sixteen hours later. Failed or uncertain
347
+ submission cannot arm the window. A Telegram Reply `approve [rationale]`, `veto [rationale]`
348
+ or `clarify [rationale]` applies only to the correlated immutable revision; other replies
349
+ stop automatic approval and require revision. Missing rationale remains absent.
350
+
351
+ The JSON proposal requires operation, target (`resource` and optional `artifact_digest`),
352
+ candidate_head, input_digest, context, options, recommendation and prerequisites; rationale
353
+ is optional. Contents are bounded and non-secret. Input digest uses the canonical service
354
+ input digest. Candidate head is separate from base head and the evidence journal commit.
355
+ Every precisely presented operation is eligible for sixteen-hour silence, including
356
+ publishing, deployment and access/privilege expansion. Fixed service scope, current head,
357
+ independently executed review/tests, bound running attempt and operation-specific OTP gates
358
+ still apply at execution. Proposal authorization never supplies credentials or runs a callback.
359
+
360
+ All proposal reads, revisions and transport acknowledgements/replies carry an explicit project.
361
+ The authority selects that project's canonical Assign journal; a proposal ID does not select a journal.
362
+ The managed request's existing project supplies this selector for ordinary `read`; Hermes uses the
363
+ accepted envelope's project for acknowledgement, reply and reconciliation. Missing selectors are
364
+ rejected, and an ID from another project's journal cannot be read or changed through the selected project.
365
+
366
+ `ace-hitl proposal show ID --project PROJECT --format json` shows decision, deadline, actual Assign claim/outcome
367
+ and a bounded history page; continue with `--history-after HISTORY_NEXT`.
368
+ `ace-hitl proposal history --project ID --query TEXT` retrieves relevant prior decisions;
369
+ continue with `--after NEXT`. Current project grants and exact requester identity gate history and show; losing project access refuses reads and excludes history, including lifecycle proposal request reads.
370
+ `ace-hitl proposal revise ID --project PROJECT --expected-revision N --operation-id revision-abcdef0123456789abcdef01 --file changed.json` supersedes the prior decision and creates a
371
+ new request with a fresh full window after acknowledgement. An unresolved claimed effect
372
+ must be reconciled before revision; known successful or proven no-effect settlement can be
373
+ followed by a new revision. Persist the proposal ID, expected source revision, stable revision operation ID and exact file before invocation. The operation ID is `revision-` followed by 24 lowercase hex digits and is unique across proposals. Exact retry returns that committed revision, including after acknowledged delivery, approval or a later revision, without creating another request or resetting the window. Changed proposal/source/content/caller bindings and stale-source new operations are refused. Supersession and new prepared revision commit atomically against effect claims. Generic lifecycle `create` rejects proposal kind; use this canonical proposal interface.
374
+
375
+ Set ACE_HITL_SOCKET and ACE_HITL_PROJECT for the living overseer. It calls
376
+ `ace-hitl proposal resolve-due --project PROJECT` on start/status/watch ticks.
377
+ The authenticated proposer queues an idempotent reconciliation wake in the
378
+ canonical Assign proposal; the command returns `queued-for-transport`, never an
379
+ approval claim. The existing installed Hermes `serve` loop polls Telegram under
380
+ its own configured transport UID and reconciles queued deadlines after polling.
381
+ No proposer subprocess opens transport configuration or impersonates transport.
382
+ Hermes holds its ingress lock across checkpoint and HITL decision transition;
383
+ unknown health/backlog defers resolution. Failed ticks remain visible while
384
+ watch/status continues. Production `--now` is rejected.
385
+
386
+ Creation requires an explicit stable ID (`proposal-` followed by 24 lowercase
387
+ hex digits). Persist that ID before invocation and retry the exact same ID,
388
+ assignment, attempt, caller and document after failure or a lost reply. Changed
389
+ binding/content is refused. Assign commits the immutable prepared lifecycle
390
+ request first; exact retry or the transport pending scan materializes it after
391
+ restart. No pending lifecycle orphan exists before canonical commit. Concurrent
392
+ materialization creates once and preserves current delivered/answered state.
393
+
394
+ Unresolved earlier same-request ingress blocks later approval delivery. Exact
395
+ reply sequence/content/time deduplication uses canonical decision history;
396
+ unseen lower sequence is applied, and changed duplicate content is refused.
397
+ History grows in the existing canonical event chain, while public responses use
398
+ bounded pages. Hermes metadata never stores message bodies.
399
+
400
+ The immutable authorization reference is the revision ID, passed to `ace-lab service request`.
401
+ Assign atomically checks the canonical proposal under its sole journal claim lock/CAS;
402
+ a forged prefix/YAML string or changed operation/target/head/input/caller cannot authorize.
403
+ A late veto before claim denies execution. After claim it records stop_requested without
404
+ rewriting performed effects; the service rechecks it before safely stoppable invocation.
405
+ An uncertain claim remains uncertain on restart and is never automatically dispatched again.
406
+ Canonical sanitized decision history shares the qjl evidence ref; no parallel executor or
407
+ execution ledger exists in HITL. This source workflow is not installed/native/Telegram or
408
+ multi-UID acceptance proof.
409
+
410
+ ### Proposal admission configuration
411
+
412
+ The protected HITL service reads proposer admission from the same trusted grants
413
+ policy as transport and project authorization. For example:
414
+
415
+ ```yaml
416
+ hitl:
417
+ service_uid: 1200
418
+ transport_uids: [1201]
419
+ proposal_uids: [1202]
420
+ authorization:
421
+ principals:
422
+ "1201":
423
+ projects: [ace]
424
+ "1202":
425
+ projects: [ace]
426
+ ```
427
+
428
+ `proposal_uids` admits authenticated kernel peers to create and revise proposals
429
+ only for their configured projects. Transport admission alone cannot create a
430
+ proposal; proposer admission cannot acknowledge delivery, fabricate ingress, or
431
+ resolve silence. Missing role or project admission refuses the operation,
432
+ including direct library calls. This is a source configuration contract;
433
+ installed deployment adoption remains part of gad.8 acceptance.
434
+
435
+ ### Controlled installed proposal verification
436
+
437
+ From an ACE checkout with Ruby, tmux and the complete dependency archives cached locally:
438
+
439
+ ```bash
440
+ bin/ace-test ace-hitl edge --filter installed_proposal_test --config-path "$PWD/ace-hitl/test/support/installed_proposal/runner.yml"
441
+ ```
442
+
443
+ The explicit fixture configuration activates the installed scenario without changing
444
+ test deadlines. Default package runs skip it. The run builds the current runtime
445
+ gem closure and installs it into an empty GEM_HOME, using local archives only.
446
+ It prints `Installed SC3 artifacts: /tmp/ace-installed-sc3/run-...`; read the
447
+ reported test receipt and that directory's `result.json`, `artifacts.json`,
448
+ `build-provenance.json`, `processes.jsonl` and `canonical-evidence.json`. A manifest/result pointer is
449
+ retained under checkout `.ace-local/installed-sc3/`.
450
+
451
+ The expected terminal result is one passing test with no failures/errors. Its
452
+ consumer asserts confirmed submission + sixteen hours, actual service/actor
453
+ process restarts, one effective authorization and one executor invocation with
454
+ a canonical verified receipt. Failed submission does not arm a deadline; lost
455
+ poll coverage stays blocked, including after fresh polling. Changed technical
456
+ scope remains refused after approval. No inbox record is manually inserted.
457
+
458
+ UTC and Telegram are test-only injected adapters. Assignment binding, kernel peer
459
+ authentication, policy, claim, execution and receipt verification remain real.
460
+ The fixture uses one host and the current UID, with an isolated tmux process and
461
+ the existing local service mode. It does not prove live Telegram, protected Herdr
462
+ launch, installed root-owned grants, or multiUID privilege separation. There is
463
+ no production `--now` or transport-override option.
464
+
465
+ Missing local dependency archives fail visibly without network fallback. A failed
466
+ consumer retains diagnostic artifacts; inspect `result.json` and `service.log`
467
+ before retrying. Each invocation retains a new run directory and requires a new
468
+ empty GEM_HOME.
@@ -14,7 +14,7 @@ module Ace
14
14
 
15
15
  desc "Ask a human via HITL and forward the request through a provider adapter"
16
16
 
17
- argument :question, required: true, desc: "Question text for the human"
17
+ option :question, type: :string, required: true, desc: "Question text for the human"
18
18
 
19
19
  option :title, type: :string, desc: "Local HITL event title (defaults to the question)"
20
20
  option :provider, type: :string, desc: "HITL provider adapter (default: ACE_HITL_PROVIDER or lab)"
@@ -26,7 +26,6 @@ module Ace
26
26
  option :"otp-result-ref", type: :string, desc: "OTP challenge: OTP-required publisher result reference"
27
27
  option :"otp-input-digest", type: :string, desc: "OTP challenge: sha256 input digest of the authorized input"
28
28
  option :"otp-expires-at", type: :string, desc: "OTP challenge: expiry as unix seconds (<= 24h ahead)"
29
- option :work, type: :string, desc: "Lab Work id (W...) - legacy binding until vs2 switches consumers"
30
29
  option :attempt, type: :string, desc: "Attempt id of the exact active attempt"
31
30
  option :project, type: :string, desc: "Lab project label (default: ace)"
32
31
  option :harness, type: :string, desc: "Lab harness label (default: lab-admin)"
@@ -45,17 +44,14 @@ module Ace
45
44
  effect = build_effect(options)
46
45
  validate_effect!(effect)
47
46
 
48
- work, assignment = require_binding!(options)
47
+ assignment = require_binding!(options)
49
48
  attempt = require_attempt!(options)
50
49
  otp = build_otp_challenge!(options)
51
50
  provider = resolve_provider(options[:provider])
52
- ref = capture_ref
53
51
 
54
52
  result = provider.ask(
55
53
  question: question,
56
54
  title: options[:title],
57
- ref: ref,
58
- work: work,
59
55
  assignment: assignment,
60
56
  attempt: attempt,
61
57
  kind: options[:kind] || "text",
@@ -67,6 +63,7 @@ module Ace
67
63
  )
68
64
 
69
65
  puts "HITL event: #{result.event_id}"
66
+ ref = result.ref
70
67
  puts "Provider: #{Providers::Lab::PROVIDER_NAME} (ref #{ref.session}/#{ref.pane}, #{Providers::Ref::SCHEMA})"
71
68
  puts "Lab request: #{result.request_id}"
72
69
  rescue Providers::ProviderUnavailableError => e
@@ -90,22 +87,10 @@ module Ace
90
87
  raise_cli_error(e.message)
91
88
  end
92
89
 
93
- # The binding is authority: exactly one of the managed
94
- # assignment binding or the legacy Work binding. An
95
- # environment variable never supplies attempt identity
96
- # (spec 8wq.t.34i).
97
90
  def require_binding!(options)
98
- work = options[:work]
99
91
  assignment = options[:assignment]
100
- if work && assignment
101
- raise_cli_error("--assignment and --work are mutually exclusive binding authorities")
102
- end
103
- if work.nil? && assignment.nil?
104
- raise_cli_error("--assignment required (managed binding), or --work for the legacy Work binding")
105
- end
106
- return [work, nil] unless work.nil?
107
-
108
- [nil, assignment]
92
+ raise_cli_error("--assignment required (the managed assignment id)") if assignment.nil? || assignment.strip.empty?
93
+ assignment
109
94
  end
110
95
 
111
96
  # The OTP challenge evidence is non-secret, structurally
@@ -144,13 +129,6 @@ module Ace
144
129
  raise_cli_error(e.message)
145
130
  end
146
131
 
147
- # Fail closed BEFORE any state is created: the reverse address is
148
- # required so the answer can be delivered back to this pane.
149
- def capture_ref
150
- Providers::Ref.from_env
151
- rescue Providers::InvalidRefError => e
152
- raise_cli_error(e.message)
153
- end
154
132
  end
155
133
  end
156
134
  end
@@ -16,9 +16,10 @@ module Ace
16
16
  desc "List answerable HITL relay requests (transport operation)"
17
17
 
18
18
  option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
19
+ option :project, type: :string, desc: "Show requests for one authorized project"
19
20
 
20
21
  def call(**options)
21
- emit(lifecycle_client.pending)
22
+ emit(LiveClient.new(boundary: lifecycle_client).pending(project: options[:project]))
22
23
  rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
23
24
  raise_lifecycle_error(e.message)
24
25
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+ require_relative "../../proposals/evaluator"
6
+
7
+ module Ace
8
+ module Hitl
9
+ module CLI
10
+ module Commands
11
+ class Proposal < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+ include LifecycleCommand
14
+ argument :operation, required: true, desc: "create, show, revise, history or resolve-due"
15
+ argument :id, required: false, desc: "Exact proposal ID"
16
+ option :assignment, type: :string
17
+ option :attempt, type: :string
18
+ option :project, type: :string
19
+ option :file, type: :string
20
+ option :"expected-revision", type: :integer, desc: "Exact source revision for revise"
21
+ option :"operation-id", type: :string, desc: "Caller-persisted revision- plus 24 lowercase hex digits"
22
+ option :format, type: :string, default: "json"
23
+ option :now, type: :string, desc: "Unavailable in production; inject a fixture clock in tests"
24
+ option :"history-after", type: :integer, desc: "Read the next bounded history page from history_next"
25
+ option :query, type: :string, desc: "Relevant prior decisions containing this text"
26
+ option :after, type: :string, desc: "History-list cursor from next"
27
+
28
+ def call(operation:, id: nil, **options)
29
+ raise_lifecycle_error("only --format json is supported") unless options[:format] == "json"
30
+ raise_lifecycle_error("--now is restricted to injected test fixtures; production uses trusted UTC clock") if options[:now]
31
+ result = case operation
32
+ when "create"
33
+ %i[assignment attempt project].each { |key| raise_lifecycle_error("--#{key} required") if options[key].to_s.empty? }
34
+ raise_lifecycle_error("stable proposal ID required") if id.to_s.empty?
35
+ lifecycle_client.proposal_create(id: id, assignment: options[:assignment], attempt: options[:attempt],
36
+ project: options[:project], document: load_document(options[:file]))
37
+ when "show"
38
+ raise_lifecycle_error("proposal ID required") if id.to_s.empty?
39
+ raise_lifecycle_error("--project required") if options[:project].to_s.empty?
40
+ lifecycle_client.proposal_show(id, project: options[:project], history_after: options[:"history-after"] || 0)
41
+ when "revise"
42
+ raise_lifecycle_error("proposal ID required") if id.to_s.empty?
43
+ raise_lifecycle_error("--expected-revision required") unless options[:"expected-revision"]
44
+ raise_lifecycle_error("--operation-id required") if options[:"operation-id"].to_s.empty?
45
+ raise_lifecycle_error("--project required") if options[:project].to_s.empty?
46
+ lifecycle_client.proposal_revise(id, project: options[:project], expected_revision: options[:"expected-revision"],
47
+ operation_id: options[:"operation-id"], document: load_document(options[:file]))
48
+ when "resolve-due"
49
+ raise_lifecycle_error("--project required") if options[:project].to_s.empty?
50
+ Proposals::Evaluator.new(boundary: lifecycle_client, project: options[:project]).call
51
+ when "history"
52
+ raise_lifecycle_error("--project required") if options[:project].to_s.empty?
53
+ lifecycle_client.proposal_history(project: options[:project], query: options[:query] || "", after: options[:after])
54
+ else raise_lifecycle_error("choose create, show, revise, history or resolve-due")
55
+ end
56
+ emit(result)
57
+ rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
58
+ raise_lifecycle_error(e.message)
59
+ end
60
+
61
+ private
62
+
63
+ def load_document(path)
64
+ raise_lifecycle_error("--file required") if path.to_s.empty?
65
+ content = File.open(path, "rb") { |file| file.read(4097) }
66
+ raise_lifecycle_error("proposal file exceeds 4096 bytes") if content.bytesize > 4096
67
+ JSON.parse(content)
68
+ rescue JSON::ParserError, SystemCallError
69
+ raise_lifecycle_error("proposal file must be readable JSON")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end