ace-hitl 0.23.2 → 0.24.1

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: 973228979e65843573a7dc67bc91566a22478ca762f3751babdbd269e0960ca9
4
- data.tar.gz: c4b09f84c60c2bfb225294d56c4bfa4abded7ced7ecc75fe830ecc8987e239ee
3
+ metadata.gz: 80aaacaa6ef70bbe80b87e00b4edee2f4cf935b679a115fd5e42f001ddb0f60b
4
+ data.tar.gz: 85b36915a24996df17f1a550f3c5ca017227482472183b0d27ff9b6bc09a65ec
5
5
  SHA512:
6
- metadata.gz: f08442f7582db2b088cbaa5f0af24d9d81bc7caccdf5e2bdfc9de5feb6a9ee9f5125f541f02e9585f4921933fa08a712fe47e80e8c668dd89330084654e13984
7
- data.tar.gz: c3ad787393cc3c045efda84016b9e2783fa8fb5f3696e4a9da9e072840dfb99e12f706c79bafc6b655f90f4ccd35f661d0f3b05932a96d00d17ed563a63b27c1
6
+ metadata.gz: 387437e8824ee6c94bc89c6c683434c129821527a6cf80dd3983b7bf875551d3e782c59e344294569722cc5531a8862ab6633f4616e46bb46e55c6224bb6d808
7
+ data.tar.gz: 8035330d8eea2aed94243161bff6e93426befa2beda2e474d1bacd80341363a7777728851d068509b1e69a5472f0f31be9b93fcf9d13c6ae64c68538aa4602d5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.1] - 2026-10-25
11
+
12
+ ### Changed
13
+
14
+ - 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.
15
+
16
+ ## [0.24.0] - 2026-10-23
17
+
18
+ ### Added
19
+
20
+ - Add read-only semantic event observations with configured-owner filtering; exclude delivery metadata and unrelated recipients.
21
+
10
22
  ## [0.23.2] - 2026-10-20
11
23
 
12
24
  ### Fixed
data/README.md CHANGED
@@ -78,3 +78,15 @@ bin/ace-test ace-hitl all
78
78
  `ace-hitl` owns HITL event semantics and the markdown contract.
79
79
  `ace-support-items` remains generic support infrastructure and does not absorb
80
80
  HITL-specific behaviour.
81
+
82
+ ### Read semantic event revisions
83
+
84
+ `ace-hitl observe --scope current --limit 500` prints one JSON object with schema
85
+ `ace.hitl.observation/v1` and a bounded `events` list. `--scope all` includes linked
86
+ worktrees of this repository. Each row contains a hashed canonical file identity,
87
+ event ID, semantic revision hash, type/status and task/assignment references.
88
+ It emits no private message or answer text. It reads active (non-special-folder) canonical events and
89
+ does not import transports, acknowledge, archive, resume or mutate events.
90
+ Delivery/poll timestamps and filesystem mtime are excluded from revisions;
91
+ content, answers and report/status changes are included. Exceeding the configured
92
+ limit refuses the entire projection rather than silently truncating it.
@@ -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-18'
7
- last-checked: '2026-10-18'
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
- Polling is the default reliability path for the requesting agent:
102
-
103
- ```bash
104
- ace-hitl wait <hitl-id>
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,24 @@
1
+ # frozen_string_literal: true
2
+ require "ace/support/cli"
3
+ require_relative "../../molecules/event_observation"
4
+ module Ace
5
+ module Hitl
6
+ module CLI
7
+ module Commands
8
+ class Observe < Ace::Support::Cli::Command
9
+ desc "Read bounded semantic event revisions without private message text"
10
+ option :scope, type: :string, default: "current", desc: "current or all linked worktrees"
11
+ option :owner, type: :string, desc: "Configured recipient owning incoming events"
12
+ option :limit, type: :integer, default: 500, desc: "Maximum event count (1..2000)"
13
+ def call(**options)
14
+ scope = options[:scope] || "current"
15
+ raise Ace::Support::Cli::Error, "invalid scope" unless %w[current all].include?(scope)
16
+ puts JSON.generate(Molecules::EventObservation.new.call(scope: scope, limit: options[:limit] || 500, owner: options[:owner]))
17
+ rescue ArgumentError => e
18
+ raise Ace::Support::Cli::Error, e.message
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/ace/hitl/cli.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "cli/commands/operator_question"
8
8
  require_relative "cli/commands/read"
9
9
  require_relative "cli/commands/show"
10
10
  require_relative "cli/commands/list"
11
+ require_relative "cli/commands/observe"
11
12
  require_relative "cli/commands/update"
12
13
  require_relative "cli/commands/wait"
13
14
  require_relative "cli/commands/deliver"
@@ -31,6 +32,7 @@ module Ace
31
32
  ["read", "Read authenticated lifecycle request facts"],
32
33
  ["show", "Show HITL event details"],
33
34
  ["list", "List HITL events"],
35
+ ["observe", "Read semantic event revisions"],
34
36
  ["update", "Update HITL event metadata or answer"],
35
37
  ["send", "Send a HITL event through the configured adapter routes"],
36
38
  ["wait", "Wait for an answer on a specific HITL event"],
@@ -58,6 +60,7 @@ module Ace
58
60
  register "read", CLI::Commands::Read
59
61
  register "show", CLI::Commands::Show
60
62
  register "list", CLI::Commands::List
63
+ register "observe", CLI::Commands::Observe
61
64
  register "update", CLI::Commands::Update
62
65
  register "send", CLI::Commands::Send
63
66
  register "wait", CLI::Commands::Wait
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ require "digest"
3
+ require "json"
4
+
5
+ module Ace
6
+ module Hitl
7
+ module Molecules
8
+ # Read-only semantic projection. No private text is emitted; delivery
9
+ # timestamps, filesystem metadata and observer acknowledgements are not
10
+ # decision changes. Canonical HITL remains the only event owner.
11
+ class EventObservation
12
+ def initialize(manager: Organisms::HitlManager.new)
13
+ @manager = manager
14
+ end
15
+
16
+ def call(scope: "current", limit: 500, owner: nil)
17
+ raise ArgumentError, "invalid observation limit" unless (1..2000).cover?(limit)
18
+ events = @manager.list(in_folder: "next", scope: scope, limit: limit, strict: true)
19
+ raise ArgumentError, "observation exceeds configured event budget" if events.size > limit
20
+ address = owner && Molecules::HitlAddressResolver.new(config: @manager.config).resolve(owner)
21
+ events = events.select { |event| incoming?(event, address) } if address
22
+ rows = events.map do |event|
23
+ semantic = [event.id, event.type, event.status, event.result, event.kind,
24
+ event.title, event.content, event.answer, event.questions,
25
+ Array(event.tags).sort, canonical(event.recipient), canonical(event.reply_to),
26
+ event.metadata.values_at("task", "assignment", "step", "step_name", "resume_instructions")]
27
+ {"key" => Digest::SHA256.hexdigest(event.file_path.to_s),
28
+ "id" => event.id, "revision" => Digest::SHA256.hexdigest(JSON.generate(canonical(semantic))),
29
+ "type" => event.type, "status" => event.status, "result" => event.result,
30
+ "task" => event.metadata["task"], "assignment" => event.metadata["assignment"]}
31
+ end
32
+ {"schema" => "ace.hitl.observation/v1", "events" => rows.sort_by { |row| row["key"] }}
33
+ end
34
+
35
+ private
36
+
37
+ # Questions created locally are outbound until actually answered. A
38
+ # worker's reply belongs to that worker, not every linked-worktree actor.
39
+ # Native instructions have no recipient; their explicit import root owns them.
40
+ def incoming?(event, address)
41
+ local = File.expand_path(event.file_path).start_with?(File.expand_path(@manager.root_dir) + File::SEPARATOR)
42
+ return local if Array(event.tags).include?("hermes-instruction")
43
+ if event.question? && event.answered?
44
+ return local && event.reply_to &&
45
+ event.reply_to["machine"] == @manager.config.dig("hitl", "machine") &&
46
+ File.expand_path(event.reply_to["folder"].to_s) == File.expand_path(@manager.root_dir)
47
+ end
48
+ recipient = event.recipient
49
+ recipient && address.all? { |key, value| recipient[key] == value }
50
+ end
51
+
52
+ def canonical(value)
53
+ case value
54
+ when Hash then value.sort.to_h.transform_values { |item| canonical(item) }
55
+ when Array then value.map { |item| canonical(item) }
56
+ else value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -94,10 +94,12 @@ module Ace
94
94
  }
95
95
  end
96
96
 
97
- def list(status: nil, kind: nil, type: nil, delivery: nil, in_folder: "next", tags: [], scope: nil)
97
+ def list(status: nil, kind: nil, type: nil, delivery: nil, in_folder: "next", tags: [], scope: nil, limit: nil, strict: false)
98
98
  effective_scope = @scope_resolver.effective_scope(scope)
99
99
  scan_results = scan_results_for_scope(effective_scope, in_folder: in_folder)
100
+ raise ArgumentError, "observation exceeds configured event budget" if limit && scan_results.size > limit
100
101
  events = load_events(scan_results)
102
+ raise ArgumentError, "incomplete HITL observation" if strict && events.size != scan_results.size
101
103
 
102
104
  events = events.select { |event| event.status == status } if status
103
105
  events = events.select { |event| event.kind == kind } if kind
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Hitl
5
- VERSION = "0.23.2"
5
+ VERSION = "0.24.1"
6
6
  end
7
7
  end
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.23.2
4
+ version: 0.24.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-20 00:00:00.000000000 Z
10
+ date: 2026-10-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-hitl-contract
@@ -160,6 +160,7 @@ files:
160
160
  - lib/ace/hitl/cli/commands/duty.rb
161
161
  - lib/ace/hitl/cli/commands/lifecycle_command.rb
162
162
  - lib/ace/hitl/cli/commands/list.rb
163
+ - lib/ace/hitl/cli/commands/observe.rb
163
164
  - lib/ace/hitl/cli/commands/operator_question.rb
164
165
  - lib/ace/hitl/cli/commands/pending.rb
165
166
  - lib/ace/hitl/cli/commands/read.rb
@@ -198,6 +199,7 @@ files:
198
199
  - lib/ace/hitl/lifecycle/tls_service.rb
199
200
  - lib/ace/hitl/live_client.rb
200
201
  - lib/ace/hitl/models/hitl_event.rb
202
+ - lib/ace/hitl/molecules/event_observation.rb
201
203
  - lib/ace/hitl/molecules/hitl_address_resolver.rb
202
204
  - lib/ace/hitl/molecules/hitl_answer_editor.rb
203
205
  - lib/ace/hitl/molecules/hitl_config_loader.rb