ace-hitl 0.9.0 → 0.10.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/CHANGELOG.md +17 -0
  3. data/README.md +33 -1
  4. data/docs/usage.md +84 -13
  5. data/lib/ace/hitl/cli/commands/ask.rb +60 -54
  6. data/lib/ace/hitl/cli/commands/cancel.rb +34 -0
  7. data/lib/ace/hitl/cli/commands/consume.rb +34 -0
  8. data/lib/ace/hitl/cli/commands/deliver.rb +32 -0
  9. data/lib/ace/hitl/cli/commands/duty.rb +29 -0
  10. data/lib/ace/hitl/cli/commands/lifecycle_command.rb +36 -0
  11. data/lib/ace/hitl/cli/commands/overseer_ack.rb +30 -0
  12. data/lib/ace/hitl/cli/commands/overseer_pending.rb +28 -0
  13. data/lib/ace/hitl/cli/commands/overseer_send.rb +35 -0
  14. data/lib/ace/hitl/cli/commands/pending.rb +29 -0
  15. data/lib/ace/hitl/cli/commands/states.rb +28 -0
  16. data/lib/ace/hitl/cli/commands/wait.rb +2 -2
  17. data/lib/ace/hitl/cli.rb +28 -1
  18. data/lib/ace/hitl/lifecycle/atomic_json.rb +79 -0
  19. data/lib/ace/hitl/lifecycle/binding.rb +27 -0
  20. data/lib/ace/hitl/lifecycle/duty.rb +43 -0
  21. data/lib/ace/hitl/lifecycle/effects.rb +254 -0
  22. data/lib/ace/hitl/lifecycle/errors.rb +27 -0
  23. data/lib/ace/hitl/lifecycle/identity.rb +66 -0
  24. data/lib/ace/hitl/lifecycle/kinds.rb +57 -0
  25. data/lib/ace/hitl/lifecycle/overseer.rb +111 -0
  26. data/lib/ace/hitl/lifecycle/store.rb +544 -0
  27. data/lib/ace/hitl/lifecycle.rb +15 -0
  28. data/lib/ace/hitl/molecules/lab_projection_observer.rb +9 -1
  29. data/lib/ace/hitl/providers/errors.rb +24 -0
  30. data/lib/ace/hitl/providers/lab/daemon_binding.rb +206 -0
  31. data/lib/ace/hitl/providers/lab.rb +124 -0
  32. data/lib/ace/hitl/providers/providers.rb +41 -0
  33. data/lib/ace/hitl/providers/ref.rb +62 -0
  34. data/lib/ace/hitl/version.rb +1 -1
  35. data/lib/ace/hitl.rb +3 -0
  36. metadata +27 -3
  37. data/lib/ace/hitl/molecules/lab_request_submitter.rb +0 -82
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c36ea0129ebd4da1a3b54f5f6cb2f1701e38a1c4176403aead696aacc954dae
4
- data.tar.gz: 4c46c595bd2dbe61968429240b6a1770895ea9f531fe84c4fe472c6146bb06df
3
+ metadata.gz: 765d5a3ec0942bb25b92c9e16bf325bc60f5f9c58e72da56e8b62f8c716817e2
4
+ data.tar.gz: b3c1015a2037ac173be8cae4ce7527fc0b099bbdf5acd419572c30992a874424
5
5
  SHA512:
6
- metadata.gz: 54cf3972cd8ada223070d61b8d09ae1fbd6c7f033ad202831e07d2d7fd902f465e424947eb558c85f6aee4eb493d754b54f11c4f6a032a2787b4054b12ebea55
7
- data.tar.gz: 98ffa471017dae7e4671958411e2a4d36b573dfaaa5ef363a39153353466919ff06c76fd6f69108289cb3d35a5781db02f3e7d6855031c38fdc340b2427d2a35
6
+ metadata.gz: a54fd6f1fc8ac167b6ccd87bb724cd469c733df2a37006e2066a5123f875365ee65ef7d91132cf284167ea359bb57df6147ecc6e75f3f9cab264a9377c31809d
7
+ data.tar.gz: 11bd3b6c7a553d5372b9cb2ee10ac87b1586e82ca90ab470e6216f041410db987842605ce7dca8595eed31e07c17c05b0453374e0cf14b2131bd65d9982aa516
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0] - 2026-09-27
11
+
12
+ ### Added
13
+ - **Generic HITL request lifecycle (spec 8wm.t.y21, M1 migration from lab-config lab-hitl)**: the generic core now lives natively in the gem under `Ace::Hitl::Lifecycle` — request store (`create`/`pending`/`states`/`deliver`/`consume`/`cancel`), kinds + OTP/secret-shape answer gates, 0400 requester-owned answer relay, 0440 merge-on-write public projection under a per-request `flock`, no time-based expiry (W651: only an answer, its consumption, or an explicit audited `cancel` ends a request), requester-declared effect callbacks executed AS THE REQUESTER (exec-argv with `{answer}`, fullmatch regex gate, bounded timeout, redacted root-only effects log, deduped escalation with `callback-ok`/`callback-escalated` projection states and the pending+escalated duty projection), and the Overseer reverse-address surface (`overseer-send`/`overseer-pending`/`overseer-ack`: bounded, type-tagged `[decyzja]`/`[pytanie]`/`[info]`, no SHA/Work/Attempt/task IDs). New CLI commands: `deliver`, `consume`, `cancel`, `pending`, `states`, `duty`, `overseer-send`, `overseer-pending`, `overseer-ack` (machine output: one JSON line). Store root: `ACE_HITL_STORE_ROOT` (default `/run/lab/hitl`); Overseer channel root: `ACE_HITL_OVERSEER_CHANNEL_ROOT` (default `/lab/state/overseer-channel`).
14
+ - **Binding policy seam**: the generic store requires a fail-closed `Lifecycle::Binding` policy; provider=lab supplies `Providers::Lab::DaemonBinding`, the minimal client of the lab daemon's read-only `hitl_binding` socket op (Work/Attempt binding authority stays lab-side, per audit 8wl.t.gad.6). Escalation spooling stays behind the store's `escalation_sink` seam (the wake/`lab_control` glue stays lab-config).
15
+ - **Provider adapter interface + provider=lab contract (spec 8wm.t.vrz)**: `ace-hitl ask` dispatches through the `Ace::Hitl::Providers` registry (selection: `--provider` flag → `ACE_HITL_PROVIDER` env → `lab`). The ask performs the local-event + transport send in ONE operation and captures the asker's reverse address fail-closed from the herdr environment (`HERDR_SESSION` / `HERDR_PANE`; versioned schema `ace.hitl.ref/v1`), persisting `provider`, `ref_schema`, `ref_session`, `ref_pane` alongside the existing `lab_request_*` fields. Pinned error model: `UnknownProviderError`, `InvalidRefError` (fail closed before any event or transport state), `ProviderUnavailableError` (transport failure; orphan event id message preserved), `UnsupportedOperationError` (`deliver(ref, answer)` lands with ace-herdr push delivery 8wm.t.vs0 + provider=lab integration 8wm.t.vs2; `wait` remains the pane-less CLI path outside the adapter).
16
+
17
+ ### Changed
18
+ - **provider=lab `ask` creates the relay request through the native lifecycle store** (binding + effect declared in-process). The external-binary transport `Providers::Lab::Transport` is DELETED (pre-1.0; supersedes the 8wm.t.vrz §7 re-homing); the orphan-event `ProviderUnavailableError` contract is preserved. File and record formats stay byte-compatible with the deployed lab consumers.
19
+ - **Zero-lab-hitl guard**: the legacy `Molecules::LabRequestSubmitter` was deleted and re-homed (same behavior, provider error model) as `Providers::Lab::Transport`, the sole owner of the lab transport binary reference. A fast guard test keeps every agent-facing ace-hitl path free of direct lab transport references, and agent-facing ask/wait output no longer names the relay binary.
20
+
21
+ ### Removed
22
+ - `Providers::Lab::Transport` and the `ACE_HITL_LAB_BIN` selection: the relay request path no longer shells out to an external binary.
23
+
24
+ ### Fixed
25
+ - **PR#336 review hardening (codex astra high)**: unique atomic-writer temporary files so competing writers can no longer delete each other's in-flight temp (store answer relay included); delivery and cancel re-validate the request incarnation and ownership under the per-request lock (a cancel+recreate of the same id can no longer redirect an answer into the new incarnation); the first projection is initialized inside the lifecycle lock behind a per-incarnation token so a broker delivery can never be regressed to `created`; effect callbacks spawn with forced exec/argv semantics (a single-element declaration can no longer reach a shell) as process group leaders whose whole group is terminated on timeout; `{answer}` substitution is literal (block-form `gsub`, no replacement-string backreferences); answer bounds are enforced on decoded UTF-8 characters with a separate byte bound, so valid multibyte answers through real IO are accepted.
26
+
10
27
  ## [0.9.0] - 2026-09-23
11
28
 
12
29
  ### Added
data/README.md CHANGED
@@ -10,11 +10,16 @@ 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 to the Lab (`--work`, 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 native lifecycle store (`--work`, 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
17
17
  - `ace-hitl wait` polls a specific HITL event until answered (`--poll-every`, `--timeout`)
18
+ - `ace-hitl deliver` answers a pending relay request from stdin (host-broker operation); executes the declared effect callback as the requester
19
+ - `ace-hitl consume` consumes one own relay request's answer (indefinite by default; `--timeout` bounds only the local wait)
20
+ - `ace-hitl cancel` cancels with an audited reason (the only way to abandon a request)
21
+ - `ace-hitl pending` / `ace-hitl states` / `ace-hitl duty` host-broker projections (pending, public lifecycle records, pending + escalated)
22
+ - `ace-hitl overseer-send` / `ace-hitl overseer-pending` / `ace-hitl overseer-ack` the Overseer reverse-address response channel
18
23
 
19
24
  `ace-hitl` is a blocker-resolution tool, not a global dashboard:
20
25
 
@@ -23,6 +28,32 @@ Canonical workflow and skill for agents:
23
28
 
24
29
  Use `ace-overseer status` for a global worktree dashboard.
25
30
 
31
+ ## Provider adapters
32
+
33
+ `ace-hitl ask` dispatches through the `Ace::Hitl::Providers` registry
34
+ (selection: `--provider` flag → `ACE_HITL_PROVIDER` env → `lab`).
35
+
36
+ - `ask` is ONE operation: it creates the local HITL event and the relay
37
+ request through the NATIVE generic lifecycle store
38
+ (`Ace::Hitl::Lifecycle`; migration spec 8wm.t.y21), then persists
39
+ `provider`, `ref_schema`, `ref_session`, `ref_pane` plus the existing
40
+ `lab_request_*` fields.
41
+ - The asker's reverse address (`ref`, versioned schema
42
+ `ace.hitl.ref/v1`: herdr session + pane) is captured fail-closed from
43
+ `HERDR_SESSION` / `HERDR_PANE`; absent or invalid values abort the ask
44
+ before any event is created or store state changes.
45
+ - Error model: `UnknownProviderError`, `InvalidRefError`,
46
+ `ProviderUnavailableError` (store-create failure; surfaces the orphan
47
+ event id when one was already created), `UnsupportedOperationError`.
48
+ - `deliver(ref, answer)` (push the answer back to the asker's pane) is
49
+ declared by the interface; provider `lab` raises
50
+ `UnsupportedOperationError` until the ace-herdr push-delivery
51
+ integration lands. `ace-hitl wait` stays the pane-less script path and
52
+ does not go through a provider.
53
+ - The generic lifecycle is provider-agnostic; all lab coupling lives in
54
+ the provider=lab seams (the `Providers::Lab::DaemonBinding` labd
55
+ binding client and the store factory), enforced by guard tests.
56
+
26
57
  ## Examples
27
58
 
28
59
  ```bash
@@ -30,6 +61,7 @@ ace-hitl list
30
61
  ace-hitl list --scope all
31
62
  ace-hitl create "Which auth strategy?" --kind decision --question "JWT or sessions?"
32
63
  ace-hitl ask "Proceed with deploy?" --work W685 --effect-arg /bin/false --effect-cwd /tmp
64
+ ace-hitl ask "Proceed with deploy?" --provider lab --work W685
33
65
  ace-hitl show abc123 --content
34
66
  ace-hitl show abc123 --scope current
35
67
  ace-hitl update abc123 --answer "Use JWT with server-side refresh tokens."
data/docs/usage.md CHANGED
@@ -102,12 +102,14 @@ ace-hitl update abc123 --move-to next
102
102
  ace-hitl update abc123 --answer "close the assignment" --resume
103
103
  ```
104
104
 
105
- ## Ask (Lab request with effect callback)
105
+ ## Ask (Provider adapter with effect callback)
106
106
 
107
- `ace-hitl ask` creates the local HITL event, forwards the question to the Lab
108
- as a HITL request bound to the event via `--ace-hitl-id`, and prints both ids.
109
- Effect declarations are validated client-side (exact bounds) and passed through
110
- verbatim; the Lab tool remains the authority.
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.
111
113
 
112
114
  ```bash
113
115
  ace-hitl ask "Proceed with deploy?" \
@@ -118,6 +120,12 @@ ace-hitl ask "Proceed with deploy?" \
118
120
 
119
121
  - `--attempt` defaults to `LAB_ATTEMPT_ID`; `--project` to `ace`;
120
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.
121
129
  - Effect flags: `--effect-match` (regex, <= 200 chars, must compile),
122
130
  `--effect-arg` (repeatable, 1..16 x 1..512 chars after the lab's
123
131
  strip-then-bounds check; whitespace-only elements fail fast, valid
@@ -126,12 +134,72 @@ ace-hitl ask "Proceed with deploy?" \
126
134
  - Whether an effect was declared is recorded on the event as
127
135
  `lab_request_effect: declared|none` so `wait` can apply the right
128
136
  terminal semantics.
129
- - The answer is always relayed unchanged; consume it with
130
- `lab-hitl consume <request-id>` when ready.
131
- - If the Lab request fails after the local event was created, the error
132
- surfaces the event id as an orphan (created but never bound to a Lab
133
- request); inspect it with `ace-hitl show <id>` and delete or re-ask
134
- as needed.
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.
146
+
147
+ ## Relay Lifecycle (Generic HITL Request Store)
148
+
149
+ The generic relay request lifecycle is native to the gem
150
+ (`Ace::Hitl::Lifecycle`; migration spec 8wm.t.y21). Requests are
151
+ file-backed, never expire on a timer, and every terminal transition
152
+ shares one per-request lock. The store root is `ACE_HITL_STORE_ROOT`
153
+ (default `/run/lab/hitl`); the Overseer channel root is
154
+ `ACE_HITL_OVERSEER_CHANNEL_ROOT` (default `/lab/state/overseer-channel`).
155
+ Machine output is one JSON line.
156
+
157
+ Operator/broker side (host-broker operations are root-only):
158
+
159
+ ```bash
160
+ ace-hitl pending # answerable requests
161
+ ace-hitl states # all public lifecycle projections
162
+ ace-hitl duty # pending + escalated projection
163
+ ace-hitl deliver hitl-0a1b2c3d4e5f6708 <<< "approved"
164
+ ```
165
+
166
+ `deliver` reads the answer from stdin, relays it unchanged (0400,
167
+ owner = requester) into `secrets/` for OTP kinds or `answers/` for
168
+ everything else, re-verifies attempt liveness under the request lock,
169
+ and then executes the declared effect callback AS THE REQUESTER:
170
+ exec-style argv (never a shell), `{answer}` substituted once per
171
+ element, optional fullmatch regex gate, bounded timeout, attempts
172
+ logged redacted in the root-only effects log, and one deduped
173
+ escalation with `effect_state: callback-escalated` in the public
174
+ projection on failure (`callback-ok` on success).
175
+
176
+ Requester side:
177
+
178
+ ```bash
179
+ ace-hitl consume hitl-0a1b2c3d4e5f6708
180
+ ace-hitl consume hitl-0a1b2c3d4e5f6708 --timeout 600
181
+ ace-hitl cancel hitl-0a1b2c3d4e5f6708 --reason "operator stopped the work"
182
+ ```
183
+
184
+ A consume timeout bounds ONLY the local wait — the request stays
185
+ pending and answerable. Cancel is the ONLY way to abandon a request;
186
+ it records `cancelled_by` and the `reason` in the public projection,
187
+ and a late answer fails closed.
188
+
189
+ Overseer reverse address (bounded, type-tagged responses):
190
+
191
+ ```bash
192
+ ace-hitl overseer-send --reply-to 321 <<< "[decyzja] Rekomendacja: A."
193
+ ace-hitl overseer-pending
194
+ ace-hitl overseer-ack msg-0123456789abcdef
195
+ ```
196
+
197
+ Responses are 1..1200 characters and must open with a type tag
198
+ (`[decyzja]`, `[pytanie]`, or `[info]`); full SHAs, Work/Attempt/task
199
+ IDs, or the word "SHA" are rejected. `overseer-send` is the overseer
200
+ user's operation; `overseer-pending`/`overseer-ack` are host-broker
201
+ (root) operations used by the transport to drain and acknowledge
202
+ relayed responses.
135
203
 
136
204
  ## Wait (Polling Default)
137
205
 
@@ -154,11 +222,14 @@ callback-ok / callback-escalated). Terminal semantics are effect-aware:
154
222
  - Effect-declaring requests keep waiting until the callback verdict
155
223
  (`callback-ok` or `callback-escalated`) appears — they never end
156
224
  silently at answer delivery; `callback-escalated` output points at
157
- `lab-hitl duty` for the escalation.
225
+ the lab duty projection for the escalation.
158
226
  - The event's `lab_request_state` records the effective state, so it
159
227
  never claims plain `answer-delivered` while an effect outcome exists.
160
228
 
161
- Relay consumption stays the agent's choice (`lab-hitl consume`).
229
+ Relay consumption stays on the operator side via `ace-hitl consume`.
230
+ `wait` is the pane-less script path: agents with a herdr pane ask
231
+ through the provider adapter and receive answers delivered back to
232
+ their pane.
162
233
 
163
234
  ## Lifecycle Event Names
164
235
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ace/support/cli"
4
+ require_relative "../../lifecycle"
4
5
  require_relative "../../atoms/hitl_effect_validator"
5
- require_relative "../../molecules/lab_request_submitter"
6
+ require_relative "../../providers/providers"
6
7
 
7
8
  module Ace
8
9
  module Hitl
@@ -11,11 +12,12 @@ module Ace
11
12
  class Ask < Ace::Support::Cli::Command
12
13
  include Ace::Support::Cli::Base
13
14
 
14
- desc "Ask a human via HITL and forward the request to the Lab"
15
+ desc "Ask a human via HITL and forward the request through a provider adapter"
15
16
 
16
17
  argument :question, required: true, desc: "Question text for the human"
17
18
 
18
19
  option :title, type: :string, desc: "Local HITL event title (defaults to the question)"
20
+ option :provider, type: :string, desc: "HITL provider adapter (default: ACE_HITL_PROVIDER or lab)"
19
21
  option :work, type: :string, desc: "Lab Work id (W...)"
20
22
  option :attempt, type: :string, desc: "Lab Attempt id (A-...); defaults to LAB_ATTEMPT_ID"
21
23
  option :project, type: :string, desc: "Lab project label (default: ace)"
@@ -32,69 +34,49 @@ module Ace
32
34
  option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
33
35
 
34
36
  def call(question:, **options)
35
- effect = {
36
- match: options[:"effect-match"],
37
- effect_args: Array(options[:"effect-arg"]),
38
- effect_cwd: options[:"effect-cwd"],
39
- effect_timeout: options[:"effect-timeout-s"]
40
- }
41
- begin
42
- Atoms::HitlEffectValidator.validate!(**effect)
43
- rescue Atoms::HitlEffectValidator::ValidationError => e
44
- raise_cli_error(e.message)
45
- end
46
- effect_declared = !!(effect[:match] || effect[:effect_cwd] || effect[:effect_timeout] ||
47
- Array(effect[:effect_args]).any?)
37
+ effect = build_effect(options)
38
+ validate_effect!(effect)
48
39
 
49
40
  work = require_work!(options)
50
- attempt = options[:attempt] || ENV["LAB_ATTEMPT_ID"]
51
- unless attempt && !attempt.strip.empty?
52
- raise_cli_error("--attempt required (or set LAB_ATTEMPT_ID)")
53
- end
54
-
55
- submitter = Molecules::LabRequestSubmitter.new
56
- request_id = submitter.generate_request_id
41
+ attempt = require_attempt!(options)
42
+ provider = resolve_provider(options[:provider])
43
+ ref = capture_ref
57
44
 
58
- manager = Ace::Hitl::Organisms::HitlManager.new
59
- event = manager.create(
60
- options[:title] || question,
61
- questions: [question]
62
- )
63
-
64
- argv = submitter.build_argv(
65
- request_id: request_id,
45
+ result = provider.ask(
46
+ question: question,
47
+ title: options[:title],
48
+ ref: ref,
66
49
  work: work,
67
50
  attempt: attempt,
68
- project: options[:project] || "ace",
69
- harness: options[:harness] || "lab-admin",
70
- plan: options[:plan] || "ace-hitl ask",
71
- question: question,
72
- ace_hitl_id: event.id,
73
- **effect
51
+ project: options[:project] || Providers::Lab::DEFAULT_PROJECT,
52
+ harness: options[:harness] || Providers::Lab::DEFAULT_HARNESS,
53
+ plan: options[:plan] || Providers::Lab::DEFAULT_PLAN,
54
+ effect: effect
74
55
  )
75
56
 
76
- begin
77
- lab_request_id = submitter.submit(argv)
78
- rescue Molecules::LabRequestSubmitter::SubmissionError => e
79
- raise_cli_error(
80
- "#{e.message}; local HITL event #{event.id} was created but never bound to a " \
81
- "Lab request (orphan) - inspect it with ace-hitl show #{event.id} and delete " \
82
- "or re-ask as needed"
83
- )
84
- end
57
+ puts "HITL event: #{result.event_id}"
58
+ puts "Provider: #{Providers::Lab::PROVIDER_NAME} (ref #{ref.session}/#{ref.pane}, #{Providers::Ref::SCHEMA})"
59
+ puts "Lab request: #{result.request_id}"
60
+ rescue Providers::ProviderUnavailableError => e
61
+ raise_cli_error(e.message)
62
+ end
85
63
 
86
- manager.update(event.id, set: {
87
- "lab_request_id" => lab_request_id,
88
- "lab_request_state" => "created",
89
- "lab_request_effect" => effect_declared ? "declared" : "none"
90
- })
64
+ private
91
65
 
92
- puts "HITL event: #{event.id}"
93
- puts "Lab request: #{lab_request_id}"
94
- puts "Answer relay: lab-hitl consume #{lab_request_id}"
66
+ def build_effect(options)
67
+ {
68
+ match: options[:"effect-match"],
69
+ effect_args: Array(options[:"effect-arg"]),
70
+ effect_cwd: options[:"effect-cwd"],
71
+ effect_timeout: options[:"effect-timeout-s"]
72
+ }
95
73
  end
96
74
 
97
- private
75
+ def validate_effect!(effect)
76
+ Atoms::HitlEffectValidator.validate!(**effect)
77
+ rescue Atoms::HitlEffectValidator::ValidationError => e
78
+ raise_cli_error(e.message)
79
+ end
98
80
 
99
81
  def require_work!(options)
100
82
  work = options[:work]
@@ -102,6 +84,30 @@ module Ace
102
84
 
103
85
  work
104
86
  end
87
+
88
+ def require_attempt!(options)
89
+ attempt = options[:attempt] || ENV["LAB_ATTEMPT_ID"]
90
+ unless attempt && !attempt.strip.empty?
91
+ raise_cli_error("--attempt required (or set LAB_ATTEMPT_ID)")
92
+ end
93
+
94
+ attempt
95
+ end
96
+
97
+ def resolve_provider(raw)
98
+ name = raw || ENV["ACE_HITL_PROVIDER"] || Providers::Lab::PROVIDER_NAME
99
+ Providers.resolve(name)
100
+ rescue Providers::UnknownProviderError => e
101
+ raise_cli_error(e.message)
102
+ end
103
+
104
+ # Fail closed BEFORE any state is created: the reverse address is
105
+ # required so the answer can be delivered back to this pane.
106
+ def capture_ref
107
+ Providers::Ref.from_env
108
+ rescue Providers::InvalidRefError => e
109
+ raise_cli_error(e.message)
110
+ end
105
111
  end
106
112
  end
107
113
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # The ONLY way to abandon a relay request: explicit, audited
11
+ # cancellation recorded in the public lifecycle projection
12
+ # (spec 8wm.t.y21 §3).
13
+ class Cancel < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+ include LifecycleCommand
16
+
17
+ desc "Cancel one HITL relay request with an audited reason"
18
+
19
+ argument :id, required: true, desc: "HITL relay request id"
20
+
21
+ option :reason, type: :string, desc: "Audited reason recorded in the public lifecycle record"
22
+
23
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
24
+
25
+ def call(id:, **options)
26
+ emit(lifecycle_store.cancel(id, reason: options[:reason] || ""))
27
+ rescue Lifecycle::Error => e
28
+ raise_lifecycle_error(e.message)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # Requester-side answer consumption. A positive timeout bounds
11
+ # ONLY the local wait and never cancels the request (W651);
12
+ # without it the wait is indefinite (spec 8wm.t.y21 §3).
13
+ class Consume < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+ include LifecycleCommand
16
+
17
+ desc "Wait for and consume the answer of one own HITL relay request"
18
+
19
+ argument :id, required: true, desc: "HITL relay request id"
20
+
21
+ option :timeout, type: :integer, desc: "Local wait bound in seconds; 0 (default) waits indefinitely"
22
+
23
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
24
+
25
+ def call(id:, **options)
26
+ emit(lifecycle_store.consume(id, timeout: options[:timeout] || 0))
27
+ rescue Lifecycle::Error => e
28
+ raise_lifecycle_error(e.message)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # Operator/broker answering of one relay request (the "respond"
11
+ # surface). The answer is read from stdin; the effect callback,
12
+ # if declared, executes after the relay (spec 8wm.t.y21 §3, §5).
13
+ class Deliver < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+ include LifecycleCommand
16
+
17
+ desc "Deliver an answer (stdin) to a pending HITL relay request"
18
+
19
+ argument :id, required: true, desc: "HITL relay request id"
20
+
21
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
22
+
23
+ def call(id:, **options)
24
+ emit(lifecycle_store.deliver(id, LifecycleCommand::STDIN_READER))
25
+ rescue Lifecycle::Error => e
26
+ raise_lifecycle_error(e.message)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # The standing-duty projection: pending + escalated (spec
11
+ # 8wm.t.y21 §7).
12
+ class Duty < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+ include LifecycleCommand
15
+
16
+ desc "Project pending and escalated HITL requests (host-broker operation)"
17
+
18
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
19
+
20
+ def call(**options)
21
+ emit(Lifecycle::Duty.project(lifecycle_store))
22
+ rescue Lifecycle::Error => e
23
+ raise_lifecycle_error(e.message)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Hitl
7
+ module CLI
8
+ module Commands
9
+ # Shared plumbing for the operator/broker lifecycle commands
10
+ # (spec 8wm.t.y21 §8): the store is built through the provider=lab
11
+ # seam and machine outputs are one JSON line, byte-compatible
12
+ # with the migrated CLI contract.
13
+ module LifecycleCommand
14
+ STDIN_READER = ->(limit) { $stdin.read(limit) }.freeze
15
+
16
+ def lifecycle_store(store: nil)
17
+ Providers::Lab.lifecycle_store(store: store)
18
+ end
19
+
20
+ def overseer
21
+ channel_root = ENV.fetch("ACE_HITL_OVERSEER_CHANNEL_ROOT", "/lab/state/overseer-channel")
22
+ Lifecycle::Overseer.new(outbox_dir: File.join(channel_root, "outbox"))
23
+ end
24
+
25
+ def emit(result)
26
+ puts JSON.generate(result)
27
+ end
28
+
29
+ def raise_lifecycle_error(message)
30
+ raise Ace::Support::Cli::Error.new(message)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # Host-broker acknowledgement of one relayed Overseer response.
11
+ class OverseerAck < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+ include LifecycleCommand
14
+
15
+ desc "Acknowledge (remove) one relayed Overseer response"
16
+
17
+ argument :id, required: true, desc: "Overseer response message id"
18
+
19
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
20
+
21
+ def call(id:, **options)
22
+ emit(overseer.ack(id))
23
+ rescue Lifecycle::Error => e
24
+ raise_lifecycle_error(e.message)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # Host-broker drain view of the Overseer response outbox.
11
+ class OverseerPending < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+ include LifecycleCommand
14
+
15
+ desc "List queued Overseer responses (host-broker operation)"
16
+
17
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
18
+
19
+ def call(**options)
20
+ emit(overseer.pending)
21
+ rescue Lifecycle::Error => e
22
+ raise_lifecycle_error(e.message)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "lifecycle_command"
5
+
6
+ module Ace
7
+ module Hitl
8
+ module CLI
9
+ module Commands
10
+ # The Root Overseer's bounded, type-tagged response through the
11
+ # reverse address (spec 8wm.t.y21 §6). The response is read from
12
+ # stdin.
13
+ class OverseerSend < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+ include LifecycleCommand
16
+
17
+ desc "Queue a bounded, type-tagged Overseer response (stdin)"
18
+
19
+ option :"reply-to", type: :string, desc: "Source message id this response answers"
20
+
21
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
22
+
23
+ def call(**options)
24
+ emit(overseer.send_response(
25
+ reply_to: options[:"reply-to"] || "",
26
+ reader: LifecycleCommand::STDIN_READER
27
+ ))
28
+ rescue Lifecycle::Error => e
29
+ raise_lifecycle_error(e.message)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end