ace-hitl 0.11.1 → 0.12.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: 2529e6b24197617c3554da5d1ff7348b19681769562259b05f6d88f469527cf1
4
- data.tar.gz: 76b205cac4da0c4d7a34f391ebf569b8c997af87d47edf8b607d602727f82582
3
+ metadata.gz: f29e133df951a7ba9c8781bc983cea5b72efe298b6544bae9c7a60626cadcdd2
4
+ data.tar.gz: e3078f625d062203d1dd94b946b3fa4503daf49c6029201682dc659c9cc8463e
5
5
  SHA512:
6
- metadata.gz: 977f6398c3f1e6a10e198c267c9b65af6f6d3ff0a2e668c3e80e4a0ad79a0fc332b9f03081fd0f4bfcd7f995a8c6b3458d5afc1bb39063dafaa4516f2b51d04c
7
- data.tar.gz: be81c083da2b5af0a5c97646b621ec2b71a6950025e0da4a608c25a2d28142ec67bba8dab032ccf06c004c3021d1e3d660a2c384a58f4925f4b4690895ea53cb
6
+ metadata.gz: edf72377db41e27f64075763543fcb4886357b9647400b634c7e06cf4ccb9285aeb87506e1ffde60f2b594e7e337f826a7beecfef3e8e32d0e28521da984679d
7
+ data.tar.gz: a71bc89e5841a1fa25b2256f7989d1ee2fb4cc830e61ae27935bb84e4f38deed4a9bd51cb5370de61633cecf911309b11219af5798798551d9dd7c7331d17a30
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-10-05
11
+
12
+ ### Added
13
+
14
+ - 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.
15
+ - Resolve immutable second-commander proposals through confirmed-delivery sixteen-hour policy and canonical Assign authorization.
16
+
17
+ ### Changed
18
+
19
+ - 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`.
20
+
21
+ - 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.
22
+
23
+ ### Fixed
24
+
25
+ - 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.
26
+
27
+ - 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.
28
+ - Bound pending IPC pages by encoded frame size while retaining all native recovery claims and project authorization; Ruby callers traverse keyset pages.
29
+ - Preserve the accepted native incarnation through answer consumption and require explicit signed-supersession retry before another submission.
30
+
10
31
  ## [0.11.1] - 2026-10-04
11
32
 
12
33
  ### 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,65 @@ 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 digest/key with Assign, and attempts exact native
131
+ submission. The accepted owner's terminal, agent and immutable native session ID
132
+ are captured before consumption and checked under the Inbox event lock. A thread
133
+ restart in the same pane refuses delivery rather than changing that original
134
+ identity. Repeated calls reuse the same event and never resubmit an uncertain
135
+ intent. A stopped watcher leaves the request/native intent visible in
136
+ `pending --project ace`; it never chooses a new pane or launches a replacement
137
+ watcher. The configured Hermes transport publishes created requests for its
138
+ explicitly registered project channels and owns their Telegram polling.
139
+
140
+ Queue acceptance (`delivered`) and wake are transport facts. Business effects
141
+ run once through the scoped service under the requester's declaration; their
142
+ separate receipt reference cannot be inferred from native delivery. Actual
143
+ consumption requires the existing trusted supervisor/observer signing context:
144
+
145
+ ```ruby
146
+ client.reconcile(request: request_id, receipt_path: signed_receipt_path)
147
+ # Explicit retry only after verified supersession/non-consumption:
148
+ client.reconcile(request: request_id, receipt_path: signed_receipt_path, retry_delivery: true)
149
+ ```
150
+
151
+ Herdr verifies the signature, exact event/attempt/digest/generation/native binding
152
+ and accepted registration under the event lock. Assign journals the verified
153
+ observation. Missing authority or signer, wrong key, changed target and stale
154
+ proof stay refused/unknown. Signed supersession leaves ordinary `deliver` and
155
+ `watch` calls queued; only `reconcile(..., retry_delivery: true)` submits again.
156
+ No elapsed-time rule establishes success or retries.
157
+ Keep the original trusted verification/signing context for unresolved events or
158
+ defer key rotation; a replacement fingerprint cannot rebind an existing event.
159
+
160
+ The shared versioned envelope is semantically owned by HITL and packaged in
161
+ `ace-hitl-contract` to preserve the acyclic HITL → Assign → Herdr → contract
162
+ graph. Its nested Hermes message and reverse reference are distinct schemas.
163
+ OTP answers and their hashes are excluded from the envelope and native delivery.
146
164
 
147
165
  ## Scoped Store Boundary (spec 8wq.t.34i)
148
166
 
@@ -204,7 +222,7 @@ root is `ACE_HITL_OVERSEER_CHANNEL_ROOT` (default
204
222
  Managed binding (the default authority):
205
223
 
206
224
  ```bash
207
- ace-hitl ask "Choose the next scope" \
225
+ ace-hitl ask --question "Choose the next scope" \
208
226
  --assignment 8x3abc --attempt a1b2c3 --project ace
209
227
  ```
210
228
 
@@ -213,11 +231,8 @@ identity, verified through the ace-assign coordinator under the
213
231
  assignment exclusion: a stale, ended, replaced, or uncertain attempt
214
232
  cannot acquire authority, and the exclusion is HELD across every
215
233
  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).
234
+ liveness check and the commit. Assignment and attempt identity are always explicit;
235
+ there is no legacy Work-only contract.
221
236
 
222
237
  Transport side:
223
238
 
@@ -285,25 +300,24 @@ ace-hitl wait abc123 --poll-every 600 --timeout 14400
285
300
  ace-hitl wait abc123 --scope current
286
301
  ```
287
302
 
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.
303
+ Managed event waits resolve `lab_request_id` and consume through authenticated
304
+ IPC. They never read folder projections or persist returned answers into the
305
+ local event. Explicit pane-less scripts can wait without any local event:
306
+
307
+ ```bash
308
+ ace-hitl wait --request hitl001 --timeout 30
309
+ ace-hitl wait --request otp001 --operation gem-push
310
+ ```
311
+
312
+ The default managed wait is indefinite; a positive timeout bounds only this
313
+ call. It never expires the request. OTP consumption requires the authorized
314
+ operation and returns its bytes only over protected IPC. Local event polling
315
+ remains available for ordinary unbound local events. Managed events cannot use
316
+ `update --resume` to launch unscoped session/shell delivery.
317
+
318
+ Installed acceptance still requires real registered Telegram ingress, native
319
+ owner observation and trusted signer/OS-user evidence in Lab. Controlled local
320
+ transport and synthetic native observations do not satisfy those gates.
307
321
 
308
322
  ## Lifecycle Event Names
309
323
 
@@ -317,3 +331,145 @@ Canonical namespace for HITL lifecycle signaling:
317
331
  - `hitl.event.resume_skipped_waiter_active`
318
332
  - `hitl.event.resume_failed`
319
333
  - `hitl.event.archived`
334
+
335
+ ## Pending recovery history across bounded IPC pages
336
+
337
+ `ace-hitl pending --project ace` continues to show unresolved native delivery
338
+ claims even after their answers have been consumed. A large retained history
339
+ must not prevent newly created questions from reaching Telegram.
340
+
341
+ The lifecycle wire `pending` operation returns `{items, next}`. `next` is an
342
+ exclusive request-ID cursor; send it as `after` with the same project for the
343
+ next page. Each response, including framing, fits the IPC byte limit. Every
344
+ page rechecks transport/project authorization. `Lifecycle::Client#pending`
345
+ collects the pages for existing Ruby/CLI callers; `pending_page(project:, after:)`
346
+ exposes a single bounded page. `read(id)` remains available for exact recovery.
347
+ No consumed claim is deleted to make the list fit. A record too large for one
348
+ page produces a classified error, never a successful truncated list.
349
+
350
+ This is a live keyset scan, not a frozen snapshot: a newly inserted ID before
351
+ the current cursor appears on the next scan. Repeated polling therefore remains
352
+ required; a cursor is not proof of delivery or native consumption.
353
+
354
+ ## Immutable second-commander proposals
355
+
356
+ `ace-hitl proposal create proposal-0123456789abcdef01234567 --assignment ID --attempt ID --project ID --file proposal.json`
357
+ 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
358
+ polling actor publishes the full precise proposal and records confirmed submission before
359
+ HITL persists delivered_at and a deadline exactly sixteen hours later. Failed or uncertain
360
+ submission cannot arm the window. A Telegram Reply `approve [rationale]`, `veto [rationale]`
361
+ or `clarify [rationale]` applies only to the correlated immutable revision; other replies
362
+ stop automatic approval and require revision. Missing rationale remains absent.
363
+
364
+ The JSON proposal requires operation, target (`resource` and optional `artifact_digest`),
365
+ candidate_head, input_digest, context, options, recommendation and prerequisites; rationale
366
+ is optional. Contents are bounded and non-secret. Input digest uses the canonical service
367
+ input digest. Candidate head is separate from base head and the evidence journal commit.
368
+ Every precisely presented operation is eligible for sixteen-hour silence, including
369
+ publishing, deployment and access/privilege expansion. Fixed service scope, current head,
370
+ independently executed review/tests, bound running attempt and operation-specific OTP gates
371
+ still apply at execution. Proposal authorization never supplies credentials or runs a callback.
372
+
373
+ `ace-hitl proposal show ID --format json` shows decision, deadline, actual Assign claim/outcome
374
+ and a bounded history page; continue with `--history-after HISTORY_NEXT`.
375
+ `ace-hitl proposal history --project ID --query TEXT` retrieves relevant prior decisions;
376
+ 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.
377
+ `ace-hitl proposal revise ID --expected-revision N --operation-id revision-abcdef0123456789abcdef01 --file changed.json` supersedes the prior decision and creates a
378
+ new request with a fresh full window after acknowledgement. An unresolved claimed effect
379
+ must be reconciled before revision; known successful or proven no-effect settlement can be
380
+ 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.
381
+
382
+ Set ACE_HITL_SOCKET and ACE_HITL_PROJECT for the living overseer. It calls
383
+ `ace-hitl proposal resolve-due --project PROJECT` on start/status/watch ticks.
384
+ The authenticated proposer queues an idempotent reconciliation wake in the
385
+ canonical Assign proposal; the command returns `queued-for-transport`, never an
386
+ approval claim. The existing installed Hermes `serve` loop polls Telegram under
387
+ its own configured transport UID and reconciles queued deadlines after polling.
388
+ No proposer subprocess opens transport configuration or impersonates transport.
389
+ Hermes holds its ingress lock across checkpoint and HITL decision transition;
390
+ unknown health/backlog defers resolution. Failed ticks remain visible while
391
+ watch/status continues. Production `--now` is rejected.
392
+
393
+ Creation requires an explicit stable ID (`proposal-` followed by 24 lowercase
394
+ hex digits). Persist that ID before invocation and retry the exact same ID,
395
+ assignment, attempt, caller and document after failure or a lost reply. Changed
396
+ binding/content is refused. Assign commits the immutable prepared lifecycle
397
+ request first; exact retry or the transport pending scan materializes it after
398
+ restart. No pending lifecycle orphan exists before canonical commit. Concurrent
399
+ materialization creates once and preserves current delivered/answered state.
400
+
401
+ Unresolved earlier same-request ingress blocks later approval delivery. Exact
402
+ reply sequence/content/time deduplication uses canonical decision history;
403
+ unseen lower sequence is applied, and changed duplicate content is refused.
404
+ History grows in the existing canonical event chain, while public responses use
405
+ bounded pages. Hermes metadata never stores message bodies.
406
+
407
+ The immutable authorization reference is the revision ID, passed to `ace-lab service request`.
408
+ Assign atomically checks the canonical proposal under its sole journal claim lock/CAS;
409
+ a forged prefix/YAML string or changed operation/target/head/input/caller cannot authorize.
410
+ A late veto before claim denies execution. After claim it records stop_requested without
411
+ rewriting performed effects; the service rechecks it before safely stoppable invocation.
412
+ An uncertain claim remains uncertain on restart and is never automatically dispatched again.
413
+ Canonical sanitized decision history shares the qjl evidence ref; no parallel executor or
414
+ execution ledger exists in HITL. This source workflow is not installed/native/Telegram or
415
+ multi-UID acceptance proof.
416
+
417
+ ### Proposal admission configuration
418
+
419
+ The protected HITL service reads proposer admission from the same trusted grants
420
+ policy as transport and project authorization. For example:
421
+
422
+ ```yaml
423
+ hitl:
424
+ service_uid: 1200
425
+ transport_uids: [1201]
426
+ proposal_uids: [1202]
427
+ authorization:
428
+ principals:
429
+ "1201":
430
+ projects: [ace]
431
+ "1202":
432
+ projects: [ace]
433
+ ```
434
+
435
+ `proposal_uids` admits authenticated kernel peers to create and revise proposals
436
+ only for their configured projects. Transport admission alone cannot create a
437
+ proposal; proposer admission cannot acknowledge delivery, fabricate ingress, or
438
+ resolve silence. Missing role or project admission refuses the operation,
439
+ including direct library calls. This is a source configuration contract;
440
+ installed deployment adoption remains part of gad.8 acceptance.
441
+
442
+ ### Controlled installed proposal verification
443
+
444
+ From an ACE checkout with Ruby, tmux and the complete dependency archives cached locally:
445
+
446
+ ```bash
447
+ bin/ace-test ace-hitl edge --filter installed_proposal_test --config-path "$PWD/ace-hitl/test/support/installed_proposal/runner.yml"
448
+ ```
449
+
450
+ The explicit fixture configuration activates the installed scenario without changing
451
+ test deadlines. Default package runs skip it. The run builds the current runtime
452
+ gem closure and installs it into an empty GEM_HOME, using local archives only.
453
+ It prints `Installed SC3 artifacts: /tmp/ace-installed-sc3/run-...`; read the
454
+ reported test receipt and that directory's `result.json`, `artifacts.json`,
455
+ `build-provenance.json`, `processes.jsonl` and `canonical-evidence.json`. A manifest/result pointer is
456
+ retained under checkout `.ace-local/installed-sc3/`.
457
+
458
+ The expected terminal result is one passing test with no failures/errors. Its
459
+ consumer asserts confirmed submission + sixteen hours, actual service/actor
460
+ process restarts, one effective authorization and one executor invocation with
461
+ a canonical verified receipt. Failed submission does not arm a deadline; lost
462
+ poll coverage stays blocked, including after fresh polling. Changed technical
463
+ scope remains refused after approval. No inbox record is manually inserted.
464
+
465
+ UTC and Telegram are test-only injected adapters. Assignment binding, kernel peer
466
+ authentication, policy, claim, execution and receipt verification remain real.
467
+ The fixture uses one host and the current UID, with an isolated tmux process and
468
+ the existing local service mode. It does not prove live Telegram, protected Herdr
469
+ launch, installed root-owned grants, or multiUID privilege separation. There is
470
+ no production `--now` or transport-override option.
471
+
472
+ Missing local dependency archives fail visibly without network fallback. A failed
473
+ consumer retains diagnostic artifacts; inspect `result.json` and `service.log`
474
+ before retrying. Each invocation retains a new run directory and requires a new
475
+ 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,73 @@
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
+ lifecycle_client.proposal_show(id, history_after: options[:"history-after"] || 0)
40
+ when "revise"
41
+ raise_lifecycle_error("proposal ID required") if id.to_s.empty?
42
+ raise_lifecycle_error("--expected-revision required") unless options[:"expected-revision"]
43
+ raise_lifecycle_error("--operation-id required") if options[:"operation-id"].to_s.empty?
44
+ lifecycle_client.proposal_revise(id, expected_revision: options[:"expected-revision"],
45
+ operation_id: options[:"operation-id"], document: load_document(options[:file]))
46
+ when "resolve-due"
47
+ raise_lifecycle_error("--project required") if options[:project].to_s.empty?
48
+ Proposals::Evaluator.new(boundary: lifecycle_client, project: options[:project]).call
49
+ when "history"
50
+ raise_lifecycle_error("--project required") if options[:project].to_s.empty?
51
+ lifecycle_client.proposal_history(project: options[:project], query: options[:query] || "", after: options[:after])
52
+ else raise_lifecycle_error("choose create, show, revise, history or resolve-due")
53
+ end
54
+ emit(result)
55
+ rescue Lifecycle::Error, Providers::ProviderUnavailableError => e
56
+ raise_lifecycle_error(e.message)
57
+ end
58
+
59
+ private
60
+
61
+ def load_document(path)
62
+ raise_lifecycle_error("--file required") if path.to_s.empty?
63
+ content = File.open(path, "rb") { |file| file.read(4097) }
64
+ raise_lifecycle_error("proposal file exceeds 4096 bytes") if content.bytesize > 4096
65
+ JSON.parse(content)
66
+ rescue JSON::ParserError, SystemCallError
67
+ raise_lifecycle_error("proposal file must be readable JSON")
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -35,10 +35,7 @@ module Ace
35
35
  "fail every client endpoint verification"
36
36
  )
37
37
  end
38
- binding_policy = Providers::Lab::CompositeBinding.new(
39
- assignment: Providers::Lab.assignment_binding(repo_root: options[:repo_root]),
40
- work: Providers::Lab::DaemonBinding.new
41
- )
38
+ binding_policy = Providers::Lab.assignment_binding(repo_root: options[:repo_root])
42
39
  service = Lifecycle::Service.new(
43
40
  root: store_root(options),
44
41
  binding: binding_policy,