ace-hitl 0.24.0 → 0.24.2
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/docs/usage.md +36 -1
- data/handbook/workflow-instructions/hitl.wf.md +15 -7
- data/lib/ace/hitl/cli/commands/ask.rb +150 -0
- data/lib/ace/hitl/cli.rb +6 -1
- data/lib/ace/hitl/molecules/hitl_route_loader.rb +22 -4
- data/lib/ace/hitl/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18d85dc5d580a07b84780113776f166ded716932248787002ada4f483113b468
|
|
4
|
+
data.tar.gz: 1744b1e6259e9b19cf17e4f184d54be65494b6221462f46afc421c8f8e953f68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e37a7a6c0b60ac6a7b91b3059548de70d0966ff775175f5044f7e220fe1e498268bad443a8c3d5ef3c82e93cdf2dbb7865a047024e4d1cf0e1d46465dc9a21c
|
|
7
|
+
data.tar.gz: c48874a7f7e4177e54b358f31e1e4b3660d08165cc4f194613d8a8dbca45ccf6e47a4fc6e5f212ab9379dbcb1a9aaa426a80812576134f516014d47f979daacb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.24.2] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add the one-turn `ask` command: create + send the question through the configured route, auto-arm the asking agent's own answer wake (ace.wake.ask/v1 request delivered through the ace-wake bridge), print the bare case id as the final stdout line, and end the turn — never block, never poll, no asker-side timeouts; undelivered routes exit 1 before any wake request is dropped, and `ask` completes on accepted-queued frames.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Formalize route recipients as first-class ordered data in the route loader.
|
|
19
|
+
|
|
20
|
+
## [0.24.1] - 2026-10-25
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Replace wait-by-polling with the semantic wake contract in the HITL workflow (PR74): one scoped watch per pending answer, end the turn, reconcile on wake.
|
|
25
|
+
|
|
10
26
|
## [0.24.0] - 2026-10-23
|
|
11
27
|
|
|
12
28
|
### Added
|
data/docs/usage.md
CHANGED
|
@@ -159,7 +159,10 @@ hitl:
|
|
|
159
159
|
adapters:
|
|
160
160
|
routes:
|
|
161
161
|
- route: overseer-terminal # display name; defaults to the adapter name
|
|
162
|
-
recipients: [overseer] # configured names
|
|
162
|
+
recipients: [overseer] # ordered: configured names, primary first;
|
|
163
|
+
# resolved to the stored destination before
|
|
164
|
+
# any adapter runs; appending a name extends
|
|
165
|
+
# coverage with zero code change
|
|
163
166
|
adapter: terminal # built-in: any ace-runtime terminal
|
|
164
167
|
runtime: herdr # tmux | herdr
|
|
165
168
|
pane: p5 # opaque pane handle (runtime-owned)
|
|
@@ -304,6 +307,38 @@ ace-hitl wait abc123 --scope current
|
|
|
304
307
|
The default wait is indefinite; a positive `--timeout` bounds only this call
|
|
305
308
|
and never expires the request. `wait` never invents an answer.
|
|
306
309
|
|
|
310
|
+
## Ask
|
|
311
|
+
|
|
312
|
+
One-turn ask: create the question, send it through the configured routes,
|
|
313
|
+
arm the asking agent's own wake for the case answer, and END THE TURN. ask
|
|
314
|
+
glues `create` + `send` + wake-arming into one command for the common case
|
|
315
|
+
where asking is the last action of a turn:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
ace-hitl ask "Continue with batch 2?" --to captain --kind approval --question "Proceed?"
|
|
319
|
+
ace-hitl ask "Auth strategy?" --to overseer --task 8xg.t.sbz.1.1
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
HITL event created: 8xhkep Continue with batch 2?
|
|
324
|
+
HITL event 8xhkep sent through 1 route(s):
|
|
325
|
+
captain (hermes_native): delivered - native transport ...
|
|
326
|
+
8xhkep
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The final stdout line is always the bare case id. ask never blocks and never
|
|
330
|
+
polls, carries no urgency language, and has no asker-side timeouts — the
|
|
331
|
+
Captain replies whenever he wants. The answer is delivered TO the agent as
|
|
332
|
+
an ace-wake wake carrying the answer content (see the ace-wake README's ask
|
|
333
|
+
bridge contract); on wake, the agent acts immediately.
|
|
334
|
+
|
|
335
|
+
Exit 0 when the question was created, the routes accepted the frame, and
|
|
336
|
+
the wake was armed — including the native-style accepted-queued case (a
|
|
337
|
+
stderr warning notes that delivery is unconfirmed; confirmation is
|
|
338
|
+
asynchronous). Exit 1 only for genuine rejects (a route refusing the
|
|
339
|
+
frame, or a concrete configuration error); no wake request is dropped for
|
|
340
|
+
a rejected ask.
|
|
341
|
+
|
|
307
342
|
## Lifecycle event names
|
|
308
343
|
|
|
309
344
|
HITL lifecycle signals use this namespace:
|
|
@@ -3,8 +3,8 @@ doc-type: workflow
|
|
|
3
3
|
title: HITL Workflow
|
|
4
4
|
purpose: Standardize human-attention handling for blocked and completed work using ace-hitl events.
|
|
5
5
|
ace-docs:
|
|
6
|
-
last-updated: '2026-10-
|
|
7
|
-
last-checked: '2026-10-
|
|
6
|
+
last-updated: '2026-10-23'
|
|
7
|
+
last-checked: '2026-10-23'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# HITL Workflow
|
|
@@ -98,11 +98,19 @@ ace-hitl show <hitl-id>
|
|
|
98
98
|
ace-hitl update <hitl-id> --answer "<human decision>"
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ace-hitl
|
|
105
|
-
|
|
101
|
+
Waiting on a pending answer is semantic, never a blocking poll. While an
|
|
102
|
+
answer is pending (for example a RubyGems OTP window of ~30–45 s), the
|
|
103
|
+
waiting session registers exactly ONE in-process `/watch` scoped to the
|
|
104
|
+
project ace-hitl transport folder (the folder holding that case's
|
|
105
|
+
request/reply files, e.g. `.ace-local/hitl`), then ends its turn. On wake:
|
|
106
|
+
run the project `ace-hitl-hermes sync` ONCE, reconcile the exact case state
|
|
107
|
+
(`ace-hitl show <hitl-id>` — answered, not merely replies>0), and act within
|
|
108
|
+
the answer window or report the precise blocker. Never block a model turn
|
|
109
|
+
with sleep/poll loops (`ace-hitl wait` blocks the turn — it is a sleep/poll
|
|
110
|
+
command, not a waiting strategy), and never introduce cron, systemd, or any
|
|
111
|
+
external heartbeat — none exists and none may be added. Observed defect
|
|
112
|
+
(eyes-20261023 OTP wait): the supervising session blocked its turn with a
|
|
113
|
+
36x sleep-8 poll loop.
|
|
106
114
|
|
|
107
115
|
### 4b) Remote delivery through configured adapter routes
|
|
108
116
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "ace/support/cli"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Hitl
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
# One-turn ask: create the question, deliver it through the configured
|
|
13
|
+
# adapter routes, arm the asking agent's own wake for the case answer,
|
|
14
|
+
# and END THE TURN. ask never blocks and never polls: the answer is
|
|
15
|
+
# delivered TO the agent as an ace-wake wake carrying the content, and
|
|
16
|
+
# the agent acts on it immediately. Asks carry no urgency language and
|
|
17
|
+
# no asker-side timeouts — the Captain replies whenever he wants.
|
|
18
|
+
class Ask < Ace::Support::Cli::Command
|
|
19
|
+
include Ace::Support::Cli::Base
|
|
20
|
+
|
|
21
|
+
# Shared with ace-wake's ask bridge (wake/ask-bridge.js): the drop-file
|
|
22
|
+
# contract that arms the asking agent's scoped answer watch.
|
|
23
|
+
WAKE_REQUEST_SCHEMA = "ace.wake.ask/v1"
|
|
24
|
+
|
|
25
|
+
desc "Ask: create, send, arm the answer wake, and end the turn"
|
|
26
|
+
|
|
27
|
+
example [
|
|
28
|
+
"ask \"Continue with batch 2?\" --to captain --kind approval --question \"Proceed?\"",
|
|
29
|
+
"ask \"Auth strategy?\" --to overseer --task 8xg.t.sbz.1.1"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
argument :title, required: false, desc: "HITL event title"
|
|
33
|
+
|
|
34
|
+
option :to, type: :string, required: true, desc: "Configured recipient name (required)"
|
|
35
|
+
option :kind, type: :string, aliases: %w[-k], desc: "Kind: clarification, decision, approval"
|
|
36
|
+
option :question, type: :string, repeat: true, aliases: %w[-Q], desc: "Question line (repeatable)"
|
|
37
|
+
option :task, type: :string, desc: "Task reference"
|
|
38
|
+
option :tags, type: :string, aliases: %w[-T], desc: "Comma-separated tags"
|
|
39
|
+
|
|
40
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
41
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
42
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
43
|
+
|
|
44
|
+
def call(title: nil, **options)
|
|
45
|
+
unless title && !title.strip.empty?
|
|
46
|
+
raise Ace::Support::Cli::Error.new("Title required")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
kind = options[:kind]
|
|
50
|
+
validate_kind!(kind) if kind
|
|
51
|
+
|
|
52
|
+
manager = Ace::Hitl::Organisms::HitlManager.new
|
|
53
|
+
event = create_question(manager, title, options)
|
|
54
|
+
unless options[:quiet]
|
|
55
|
+
puts "HITL event created: #{event.id} #{event.title}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
deliver(event, options)
|
|
59
|
+
arm_wake(event, manager)
|
|
60
|
+
# The final stdout line is always the bare case id.
|
|
61
|
+
puts event.id
|
|
62
|
+
rescue Ace::Hitl::Molecules::HitlAddressResolver::UnknownRecipientError,
|
|
63
|
+
Ace::Hitl::Molecules::HitlAddressResolver::InvalidAddressError,
|
|
64
|
+
Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError,
|
|
65
|
+
Ace::Hitl::Adapters::Error,
|
|
66
|
+
Ace::Hitl::Molecules::HitlRouteLoader::NoRoutesError,
|
|
67
|
+
Ace::Hitl::Molecules::HitlRouteLoader::InvalidRouteError => e
|
|
68
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def create_question(manager, title, options)
|
|
74
|
+
manager.create(
|
|
75
|
+
title,
|
|
76
|
+
to: options[:to],
|
|
77
|
+
kind: options[:kind],
|
|
78
|
+
type: "question",
|
|
79
|
+
task: options[:task],
|
|
80
|
+
questions: Array(options[:question]).map(&:strip).reject(&:empty?),
|
|
81
|
+
tags: parse_tags(options[:tags])
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def deliver(event, options)
|
|
86
|
+
report = Organisms::HitlSender.new.send(event.id)
|
|
87
|
+
rejected = report.results.select { |result| result.status == :failed }
|
|
88
|
+
unless rejected.empty?
|
|
89
|
+
# A genuine reject (ContractError, unknown adapter): the frame
|
|
90
|
+
# never went through, so arming a wake would strand the ask.
|
|
91
|
+
detail = rejected.map { |result| "#{result.route}: #{result.detail}" }.join("; ")
|
|
92
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{event.id}' rejected: #{detail}")
|
|
93
|
+
end
|
|
94
|
+
unless options[:quiet]
|
|
95
|
+
puts "HITL event #{report.event_id} sent through #{report.results.size} route(s):"
|
|
96
|
+
report.results.each do |result|
|
|
97
|
+
detail = result.detail ? " - #{result.detail}" : ""
|
|
98
|
+
puts " #{result.route} (#{result.adapter}): #{result.status}#{detail}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
# Native-style transports accept first and confirm later: an
|
|
102
|
+
# ACCEPTED QUEUED frame is ask-complete. The answer wake fires
|
|
103
|
+
# whenever the transport confirms — delivered-later states are
|
|
104
|
+
# asynchronous, never an asker-side failure.
|
|
105
|
+
queued = report.results.select { |result| result.status == :unavailable }
|
|
106
|
+
unless queued.empty?
|
|
107
|
+
warn "ace-hitl: warning: delivery unconfirmed (queued on " \
|
|
108
|
+
"#{queued.map { |result| result.route }.join(", ")}); the answer wake fires when the transport delivers"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Drop the wake request the ace-wake extension consumes through its
|
|
113
|
+
# own reconcile pass and turns into a scoped watch on the case file:
|
|
114
|
+
# when the answer lands, the wake carries the answer content. The
|
|
115
|
+
# atomic replace keeps the consumer from reading a torn request.
|
|
116
|
+
def arm_wake(event, manager)
|
|
117
|
+
requests_dir = File.join(File.expand_path(manager.root_dir), "wake-requests")
|
|
118
|
+
FileUtils.mkdir_p(requests_dir)
|
|
119
|
+
request = {
|
|
120
|
+
"schema" => WAKE_REQUEST_SCHEMA,
|
|
121
|
+
"kind" => "ask",
|
|
122
|
+
"case_id" => event.id,
|
|
123
|
+
"case_path" => File.expand_path(event.file_path)
|
|
124
|
+
}
|
|
125
|
+
temporary = File.join(requests_dir, ".#{event.id}-#{SecureRandom.hex(8)}.tmp")
|
|
126
|
+
final = File.join(requests_dir, "#{event.id}.json")
|
|
127
|
+
File.write(temporary, JSON.generate(request))
|
|
128
|
+
File.rename(temporary, final)
|
|
129
|
+
final
|
|
130
|
+
ensure
|
|
131
|
+
File.unlink(temporary) if temporary && File.exist?(temporary)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_kind!(kind)
|
|
135
|
+
allowed = %w[clarification decision approval]
|
|
136
|
+
return if allowed.include?(kind)
|
|
137
|
+
|
|
138
|
+
raise Ace::Support::Cli::Error.new("Invalid kind '#{kind}'. Allowed: #{allowed.join(", ")}")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def parse_tags(raw)
|
|
142
|
+
return [] unless raw
|
|
143
|
+
|
|
144
|
+
raw.split(",").map(&:strip).reject(&:empty?)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
data/lib/ace/hitl/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "ace/support/cli"
|
|
4
4
|
require_relative "../hitl/version"
|
|
5
5
|
require_relative "cli/commands/create"
|
|
6
|
+
require_relative "cli/commands/ask"
|
|
6
7
|
require_relative "cli/commands/send"
|
|
7
8
|
require_relative "cli/commands/operator_question"
|
|
8
9
|
require_relative "cli/commands/read"
|
|
@@ -34,6 +35,7 @@ module Ace
|
|
|
34
35
|
["list", "List HITL events"],
|
|
35
36
|
["observe", "Read semantic event revisions"],
|
|
36
37
|
["update", "Update HITL event metadata or answer"],
|
|
38
|
+
["ask", "Ask: create, send, arm the answer wake, and end the turn"],
|
|
37
39
|
["send", "Send a HITL event through the configured adapter routes"],
|
|
38
40
|
["wait", "Wait for an answer on a specific HITL event"],
|
|
39
41
|
["deliver", "Deliver an answer (stdin) to a pending HITL relay request"],
|
|
@@ -52,7 +54,9 @@ module Ace
|
|
|
52
54
|
"ace-hitl create \"Work done\" --type report --result success --task 8xg.t.sbz.1.0",
|
|
53
55
|
"ace-hitl update abc123 --answer \"Use JWT with refresh tokens\"",
|
|
54
56
|
"ace-hitl send 8xhkep",
|
|
55
|
-
"ace-hitl wait abc123 --poll-every 600 --timeout 14400"
|
|
57
|
+
"ace-hitl wait abc123 --poll-every 600 --timeout 14400",
|
|
58
|
+
"ace-hitl ask \"Continue?\" --to captain --kind approval --question \"Proceed?\"",
|
|
59
|
+
"ace-hitl ask \"Auth strategy?\" --to overseer --task 8xg.t.sbz.1.1"
|
|
56
60
|
].freeze
|
|
57
61
|
|
|
58
62
|
register "create", CLI::Commands::Create
|
|
@@ -62,6 +66,7 @@ module Ace
|
|
|
62
66
|
register "list", CLI::Commands::List
|
|
63
67
|
register "observe", CLI::Commands::Observe
|
|
64
68
|
register "update", CLI::Commands::Update
|
|
69
|
+
register "ask", CLI::Commands::Ask
|
|
65
70
|
register "send", CLI::Commands::Send
|
|
66
71
|
register "wait", CLI::Commands::Wait
|
|
67
72
|
register "deliver", CLI::Commands::Deliver
|
|
@@ -20,6 +20,15 @@ module Ace
|
|
|
20
20
|
# single path segment (no dots).
|
|
21
21
|
ROUTE_NAME_PATTERN = /\A[a-z0-9][a-z0-9_-]*\z/i.freeze
|
|
22
22
|
|
|
23
|
+
# A route's `recipients` list is first-class ORDERED data: each entry
|
|
24
|
+
# is a configured recipient name, position is meaningful (primary
|
|
25
|
+
# first), and the loader preserves the list verbatim — never
|
|
26
|
+
# reordered, never deduped. Selection matches an event addressed to
|
|
27
|
+
# ANY listed name, so extending coverage (a second-captain entry
|
|
28
|
+
# tomorrow) is a configuration change only: add the name under
|
|
29
|
+
# `hitl.recipients` and append it to the route's list. Duplicates
|
|
30
|
+
# carry no order meaning and are rejected as invalid.
|
|
31
|
+
|
|
23
32
|
# @param config [Hash] merged hitl namespace config
|
|
24
33
|
# @return [Array<Hash>] validated string-keyed routes
|
|
25
34
|
# @raise [NoRoutesError, InvalidRouteError]
|
|
@@ -66,10 +75,19 @@ module Ace
|
|
|
66
75
|
|
|
67
76
|
normalized["route"] = route
|
|
68
77
|
normalized["adapter"] = adapter
|
|
69
|
-
if normalized.key?("recipients")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
if normalized.key?("recipients")
|
|
79
|
+
list = normalized["recipients"]
|
|
80
|
+
unless list.is_a?(Array) && !list.empty? && list.all? { |name| name.is_a?(String) && !name.strip.empty? }
|
|
81
|
+
raise InvalidRouteError, "route '#{route}' recipients must be a non-empty list of configured recipient names"
|
|
82
|
+
end
|
|
83
|
+
names = list.map(&:strip)
|
|
84
|
+
duplicates = names.tally.select { |_, count| count > 1 }.keys
|
|
85
|
+
unless duplicates.empty?
|
|
86
|
+
raise InvalidRouteError,
|
|
87
|
+
"route '#{route}' recipients list repeats #{duplicates.join(", ")}: recipients are ordered data " \
|
|
88
|
+
"and must list each configured name once"
|
|
89
|
+
end
|
|
90
|
+
normalized["recipients"] = names
|
|
73
91
|
end
|
|
74
92
|
normalized
|
|
75
93
|
end
|
data/lib/ace/hitl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-hitl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.24.
|
|
4
|
+
version: 0.24.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-hitl-contract
|
|
@@ -153,6 +153,7 @@ files:
|
|
|
153
153
|
- lib/ace/hitl/atoms/hitl_file_pattern.rb
|
|
154
154
|
- lib/ace/hitl/atoms/hitl_id_formatter.rb
|
|
155
155
|
- lib/ace/hitl/cli.rb
|
|
156
|
+
- lib/ace/hitl/cli/commands/ask.rb
|
|
156
157
|
- lib/ace/hitl/cli/commands/cancel.rb
|
|
157
158
|
- lib/ace/hitl/cli/commands/consume.rb
|
|
158
159
|
- lib/ace/hitl/cli/commands/create.rb
|