ace-hitl 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/hitl/config.yml +3 -0
  3. data/.ace-defaults/nav/protocols/skill-sources/ace-hitl.yml +13 -0
  4. data/.ace-defaults/nav/protocols/wfi-sources/ace-hitl.yml +13 -0
  5. data/CHANGELOG.md +128 -0
  6. data/README.md +42 -0
  7. data/Rakefile +10 -0
  8. data/docs/demo/ace-hitl-scope-and-answer.tape.yml +37 -0
  9. data/docs/demo/fixtures/demo-context.md +7 -0
  10. data/docs/usage.md +112 -0
  11. data/exe/ace-hitl +18 -0
  12. data/handbook/README.md +14 -0
  13. data/handbook/skills/as-hitl/SKILL.md +29 -0
  14. data/handbook/workflow-instructions/hitl.wf.md +110 -0
  15. data/lib/ace/hitl/atoms/hitl_file_pattern.rb +20 -0
  16. data/lib/ace/hitl/atoms/hitl_id_formatter.rb +15 -0
  17. data/lib/ace/hitl/cli/commands/create.rb +76 -0
  18. data/lib/ace/hitl/cli/commands/list.rb +63 -0
  19. data/lib/ace/hitl/cli/commands/show.rb +79 -0
  20. data/lib/ace/hitl/cli/commands/update.rb +127 -0
  21. data/lib/ace/hitl/cli/commands/wait.rb +74 -0
  22. data/lib/ace/hitl/cli.rb +62 -0
  23. data/lib/ace/hitl/models/hitl_event.rb +32 -0
  24. data/lib/ace/hitl/molecules/hitl_answer_editor.rb +25 -0
  25. data/lib/ace/hitl/molecules/hitl_config_loader.rb +50 -0
  26. data/lib/ace/hitl/molecules/hitl_creator.rb +207 -0
  27. data/lib/ace/hitl/molecules/hitl_display_formatter.rb +53 -0
  28. data/lib/ace/hitl/molecules/hitl_loader.rb +113 -0
  29. data/lib/ace/hitl/molecules/hitl_resolver.rb +30 -0
  30. data/lib/ace/hitl/molecules/hitl_scanner.rb +42 -0
  31. data/lib/ace/hitl/molecules/resume_dispatcher.rb +99 -0
  32. data/lib/ace/hitl/molecules/worktree_scope_resolver.rb +77 -0
  33. data/lib/ace/hitl/organisms/hitl_manager.rb +462 -0
  34. data/lib/ace/hitl/version.rb +7 -0
  35. data/lib/ace/hitl.rb +24 -0
  36. metadata +164 -0
@@ -0,0 +1,462 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "fileutils"
5
+ require "pathname"
6
+ require "ace/support/items"
7
+ require_relative "../molecules/hitl_config_loader"
8
+ require_relative "../molecules/hitl_scanner"
9
+ require_relative "../molecules/hitl_loader"
10
+ require_relative "../molecules/hitl_creator"
11
+ require_relative "../molecules/hitl_answer_editor"
12
+ require_relative "../molecules/resume_dispatcher"
13
+ require_relative "../molecules/worktree_scope_resolver"
14
+
15
+ module Ace
16
+ module Hitl
17
+ module Organisms
18
+ class HitlManager
19
+ class AmbiguousReferenceError < StandardError
20
+ attr_reader :ref, :matches
21
+
22
+ def initialize(ref, matches)
23
+ @ref = ref
24
+ @matches = matches
25
+ super("Ambiguous HITL reference '#{ref}'")
26
+ end
27
+ end
28
+
29
+ attr_reader :root_dir, :last_list_total, :last_folder_counts
30
+
31
+ def initialize(root_dir: nil, config: nil, scope_resolver: nil, resume_dispatcher: nil)
32
+ @config = config || load_config
33
+ @configured_root_setting = @config.dig("hitl", "root_dir") || Molecules::HitlConfigLoader::DEFAULT_ROOT_DIR
34
+ @root_dir = root_dir || resolve_root_dir
35
+ @scope_resolver = scope_resolver || Molecules::WorktreeScopeResolver.new
36
+ @resume_dispatcher = resume_dispatcher || Molecules::ResumeDispatcher.new
37
+ end
38
+
39
+ def create(title, **options)
40
+ ensure_root_dir
41
+ creator = Molecules::HitlCreator.new(root_dir: @root_dir, config: @config)
42
+ creator.create(title, **options)
43
+ end
44
+
45
+ def show(ref, scope: nil)
46
+ effective_scope = @scope_resolver.effective_scope(scope)
47
+ roots = hitl_roots_for_scope(effective_scope)
48
+ current_root = current_hitl_root
49
+
50
+ resolved = resolve_from_roots(ref, roots, strict_ambiguity: true)
51
+
52
+ fallback_used = false
53
+ if resolved.nil? && scope.nil? && effective_scope == "current"
54
+ resolved = resolve_from_roots(ref, hitl_roots_for_scope("all"), strict_ambiguity: true)
55
+ effective_scope = "all" if resolved
56
+ fallback_used = !resolved.nil?
57
+ end
58
+
59
+ return nil unless resolved
60
+
61
+ {
62
+ event: resolved[:event],
63
+ effective_scope: effective_scope,
64
+ fallback_used: fallback_used,
65
+ resolved_hitl_root: resolved[:hitl_root],
66
+ resolved_worktree_root: resolved[:worktree_root],
67
+ resolved_outside_current: !current_root.nil? && resolved[:hitl_root] != current_root
68
+ }
69
+ end
70
+
71
+ def list(status: nil, kind: nil, in_folder: "next", tags: [], scope: nil)
72
+ effective_scope = @scope_resolver.effective_scope(scope)
73
+ scan_results = scan_results_for_scope(effective_scope, in_folder: in_folder)
74
+ events = load_events(scan_results)
75
+
76
+ events = events.select { |event| event.status == status } if status
77
+ events = events.select { |event| event.kind == kind } if kind
78
+ events = filter_by_tags(events, tags) if tags.any?
79
+
80
+ events
81
+ end
82
+
83
+ def update(ref, set: {}, add: {}, remove: {}, move_to: nil, answer: nil, scope: nil)
84
+ resolved = resolve_for_mutation(ref, scope: scope)
85
+ return nil unless resolved
86
+
87
+ event = resolved[:event]
88
+ hitl_root = resolved[:hitl_root] || @root_dir
89
+
90
+ has_field_updates = [set, add, remove].any? { |h| h && !h.empty? }
91
+ if has_field_updates && !answer.nil?
92
+ apply_field_and_answer_updates(event.file_path, set: set, add: add, remove: remove, answer: answer)
93
+ elsif has_field_updates
94
+ Ace::Support::Items::Molecules::FieldUpdater.update(
95
+ event.file_path,
96
+ set: set,
97
+ add: add,
98
+ remove: remove
99
+ )
100
+ end
101
+
102
+ apply_answer_update(event.file_path, answer) unless answer.nil?
103
+
104
+ current_path = event.path
105
+ current_special = event.special_folder
106
+ if move_to
107
+ mover = Ace::Support::Items::Molecules::FolderMover.new(hitl_root)
108
+ new_path = if Ace::Support::Items::Atoms::SpecialFolderDetector.move_to_root?(move_to)
109
+ mover.move_to_root(event)
110
+ else
111
+ mover.move(event, to: move_to, date: parse_archive_date(event))
112
+ end
113
+ current_path = new_path
114
+ current_special = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
115
+ new_path,
116
+ root: hitl_root
117
+ )
118
+ end
119
+
120
+ loader = Molecules::HitlLoader.new
121
+ loader.load(current_path, id: event.id, special_folder: current_special)
122
+ end
123
+
124
+ def wait_for_answer(ref, scope: nil, poll_every: 600, timeout: 14_400, waiter: {}, now_proc: nil, sleeper: nil)
125
+ poll_every = normalize_poll_seconds(poll_every)
126
+ timeout = normalize_timeout_seconds(timeout)
127
+ now_proc ||= -> { Time.now.utc }
128
+ sleeper ||= ->(seconds) { sleep(seconds) }
129
+
130
+ started_at = now_proc.call
131
+ deadline = started_at + timeout
132
+ waiter_session_id = waiter[:session_id].to_s.strip
133
+ waiter_provider = waiter[:provider].to_s.strip
134
+
135
+ loop do
136
+ current = show(ref, scope: scope)
137
+ return {status: :not_found} unless current
138
+
139
+ event = current[:event]
140
+ now = now_proc.call
141
+ refresh_waiter_lease(
142
+ event,
143
+ now: now,
144
+ deadline: deadline,
145
+ poll_every: poll_every,
146
+ waiter_session_id: waiter_session_id,
147
+ waiter_provider: waiter_provider,
148
+ scope: scope
149
+ )
150
+
151
+ if event.answered?
152
+ update(event.id,
153
+ set: {
154
+ "waiter_state" => "answered",
155
+ "waiter_last_seen_at" => now.iso8601
156
+ },
157
+ scope: scope
158
+ )
159
+ refreshed = show(event.id, scope: scope)&.dig(:event) || event
160
+ return {status: :answered, event: refreshed}
161
+ end
162
+
163
+ if now >= deadline
164
+ update(event.id, set: {"waiter_state" => "timed_out"}, scope: scope)
165
+ return {status: :timeout, event: event}
166
+ end
167
+
168
+ sleep_seconds = [poll_every, (deadline - now).ceil].min
169
+ sleeper.call(sleep_seconds) if sleep_seconds.positive?
170
+ end
171
+ end
172
+
173
+ def dispatch_resume(ref, scope: nil, now: Time.now.utc)
174
+ current = show(ref, scope: scope)
175
+ return {status: :not_found} unless current
176
+
177
+ event = current[:event]
178
+ answer = event.answer.to_s
179
+ return {status: :no_answer, event: event} if answer.strip.empty?
180
+
181
+ if waiter_active?(event, now: now)
182
+ return {status: :waiter_active, event: event}
183
+ end
184
+
185
+ result = @resume_dispatcher.dispatch(event: event, answer: answer, now: now)
186
+ unless result.success?
187
+ update(event.id,
188
+ set: {
189
+ "resume_dispatch_status" => "failed",
190
+ "resume_dispatch_attempted_at" => now.iso8601,
191
+ "resume_dispatch_error" => result.error
192
+ },
193
+ scope: scope
194
+ )
195
+ return {status: :failed, event: event, error: result.error}
196
+ end
197
+
198
+ update(event.id,
199
+ set: {
200
+ "resume_dispatch_status" => "dispatched",
201
+ "resume_dispatch_attempted_at" => now.iso8601,
202
+ "resumed_at" => now.iso8601,
203
+ "resumed_by" => result.details,
204
+ "waiter_state" => "resumed",
205
+ "resume_dispatch_error" => nil
206
+ },
207
+ scope: scope
208
+ )
209
+
210
+ note = "Work resumed at #{now.iso8601} via #{result.mode} (#{result.details})."
211
+ append_resume_note(event.id, note, scope: scope)
212
+ archived = update(event.id, move_to: "archive", scope: scope)
213
+
214
+ {status: :dispatched, event: archived, mode: result.mode, details: result.details}
215
+ end
216
+
217
+ private
218
+
219
+ def load_config
220
+ Molecules::HitlConfigLoader.load
221
+ end
222
+
223
+ def resolve_root_dir
224
+ Molecules::HitlConfigLoader.root_dir(@config)
225
+ end
226
+
227
+ def ensure_root_dir
228
+ FileUtils.mkdir_p(@root_dir) unless Dir.exist?(@root_dir)
229
+ end
230
+
231
+ def resolve_for_mutation(ref, scope: nil)
232
+ effective_scope = @scope_resolver.effective_scope(scope)
233
+ roots = hitl_roots_for_scope(effective_scope)
234
+
235
+ resolved = resolve_from_roots(ref, roots, strict_ambiguity: true)
236
+ if resolved.nil? && scope.nil? && effective_scope == "current"
237
+ resolved = resolve_from_roots(ref, hitl_roots_for_scope("all"), strict_ambiguity: true)
238
+ end
239
+
240
+ resolved
241
+ end
242
+
243
+ def resolve_from_roots(ref, hitl_roots, strict_ambiguity: false)
244
+ root_results = hitl_roots.uniq.filter_map do |hitl_root|
245
+ scanner = Molecules::HitlScanner.new(hitl_root)
246
+ results = scanner.scan
247
+ next if results.empty?
248
+
249
+ {hitl_root: hitl_root, results: results}
250
+ end
251
+ return nil if root_results.empty?
252
+
253
+ all_results = root_results.flat_map { |entry| entry[:results] }
254
+ resolver = Ace::Support::Items::Molecules::ShortcutResolver.new(all_results)
255
+ matches = resolver.all_matches(ref)
256
+ return nil if matches.empty?
257
+ if strict_ambiguity && matches.size > 1
258
+ raise AmbiguousReferenceError.new(ref, matches)
259
+ end
260
+
261
+ scan_result = resolver.resolve(ref, on_ambiguity: nil)
262
+ return nil unless scan_result
263
+
264
+ root_entry = root_results.find { |entry| entry[:results].include?(scan_result) }
265
+ hitl_root = root_entry && root_entry[:hitl_root]
266
+ event = load_event(scan_result)
267
+ return nil unless event
268
+
269
+ {
270
+ event: event,
271
+ hitl_root: hitl_root,
272
+ worktree_root: worktree_root_from_hitl_root(hitl_root)
273
+ }
274
+ end
275
+
276
+ def scan_results_for_scope(scope, in_folder:)
277
+ total = 0
278
+ folder_counts = Hash.new(0)
279
+
280
+ results = hitl_roots_for_scope(scope).uniq.flat_map do |hitl_root|
281
+ scanner = Molecules::HitlScanner.new(hitl_root)
282
+ scoped_results = scanner.scan_in_folder(in_folder)
283
+ total += scanner.last_scan_total.to_i
284
+ (scanner.last_folder_counts || {}).each { |key, value| folder_counts[key] += value }
285
+ scoped_results
286
+ end
287
+
288
+ @last_list_total = total
289
+ @last_folder_counts = folder_counts
290
+ results
291
+ end
292
+
293
+ def load_events(scan_results)
294
+ scan_results.filter_map { |scan_result| load_event(scan_result) }
295
+ end
296
+
297
+ def load_event(scan_result)
298
+ loader = Molecules::HitlLoader.new
299
+ loader.load(scan_result.dir_path,
300
+ id: scan_result.id,
301
+ special_folder: scan_result.special_folder)
302
+ end
303
+
304
+ def hitl_roots_for_scope(scope)
305
+ roots = @scope_resolver.worktree_roots(scope: scope)
306
+ roots = [@scope_resolver.current_worktree_root].compact if roots.empty?
307
+ roots = [nil] if roots.empty?
308
+
309
+ roots.filter_map do |worktree_root|
310
+ next @root_dir if worktree_root.nil?
311
+ next @configured_root_setting if Pathname.new(@configured_root_setting).absolute?
312
+
313
+ File.join(worktree_root, @configured_root_setting)
314
+ end.uniq
315
+ end
316
+
317
+ def current_hitl_root
318
+ root = @scope_resolver.current_worktree_root
319
+ return @root_dir if root.nil?
320
+ return @configured_root_setting if Pathname.new(@configured_root_setting).absolute?
321
+
322
+ File.join(root, @configured_root_setting)
323
+ end
324
+
325
+ def worktree_root_from_hitl_root(hitl_root)
326
+ return nil if hitl_root.nil?
327
+ return nil if Pathname.new(@configured_root_setting).absolute?
328
+
329
+ expanded = File.expand_path(hitl_root)
330
+ relative = @configured_root_setting.sub(%r{\A\./}, "")
331
+ suffix = "/#{relative}"
332
+ return nil unless expanded.end_with?(suffix)
333
+
334
+ expanded[0...-suffix.length]
335
+ end
336
+
337
+ def filter_by_tags(events, tags)
338
+ events.select { |event| tags.any? { |tag| event.tags.include?(tag) } }
339
+ end
340
+
341
+ def apply_answer_update(file_path, answer)
342
+ content = File.read(file_path)
343
+ frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
344
+
345
+ updated_body = Molecules::HitlAnswerEditor.apply(body, answer)
346
+ answered = !answer.to_s.strip.empty?
347
+ frontmatter["answered"] = answered
348
+ frontmatter["status"] = answered ? "answered" : (frontmatter["status"] || "pending")
349
+ frontmatter["answered_at"] = answered ? Time.now.utc : nil
350
+
351
+ new_content = Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, updated_body)
352
+ tmp_path = "#{file_path}.tmp.#{Process.pid}"
353
+ File.write(tmp_path, new_content)
354
+ File.rename(tmp_path, file_path)
355
+ ensure
356
+ File.unlink(tmp_path) if tmp_path && File.exist?(tmp_path)
357
+ end
358
+
359
+ def apply_field_and_answer_updates(file_path, set:, add:, remove:, answer:)
360
+ File.open(file_path, File::RDWR) do |file|
361
+ file.flock(File::LOCK_EX)
362
+ file.rewind
363
+
364
+ content = file.read
365
+ frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
366
+ body = body.sub(/\A\n/, "")
367
+
368
+ Ace::Support::Items::Molecules::FieldUpdater.apply_set(frontmatter, set)
369
+ Ace::Support::Items::Molecules::FieldUpdater.apply_add(frontmatter, add)
370
+ Ace::Support::Items::Molecules::FieldUpdater.apply_remove(frontmatter, remove)
371
+
372
+ updated_body = Molecules::HitlAnswerEditor.apply(body, answer)
373
+ answered = !answer.to_s.strip.empty?
374
+ frontmatter["answered"] = answered
375
+ frontmatter["status"] = answered ? "answered" : (frontmatter["status"] || "pending")
376
+ frontmatter["answered_at"] = answered ? Time.now.utc : nil
377
+
378
+ new_content = Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, updated_body)
379
+ file.rewind
380
+ file.truncate(0)
381
+ file.write(new_content)
382
+ file.flush
383
+ file.fsync
384
+ end
385
+ end
386
+
387
+ def parse_archive_date(event)
388
+ raw = event.metadata["answered_at"] || event.metadata["created_at"] || event.created_at
389
+ return nil unless raw
390
+
391
+ case raw
392
+ when Time then raw
393
+ when DateTime then raw.to_time
394
+ else
395
+ Time.parse(raw.to_s)
396
+ end
397
+ rescue StandardError
398
+ nil
399
+ end
400
+
401
+ def refresh_waiter_lease(event, now:, deadline:, poll_every:, waiter_session_id:, waiter_provider:, scope:)
402
+ set = {
403
+ "waiter_state" => "waiting",
404
+ "waiter_last_seen_at" => now.iso8601,
405
+ "waiter_poll_every_sec" => poll_every,
406
+ "waiter_timeout_at" => deadline.iso8601
407
+ }
408
+ set["waiter_session_id"] = waiter_session_id unless waiter_session_id.empty?
409
+ set["waiter_provider"] = waiter_provider unless waiter_provider.empty?
410
+ update(event.id, set: set, scope: scope)
411
+ end
412
+
413
+ def waiter_active?(event, now:)
414
+ return false unless event.metadata["waiter_state"].to_s == "waiting"
415
+
416
+ last_seen = parse_time(event.metadata["waiter_last_seen_at"])
417
+ return false unless last_seen
418
+
419
+ interval = event.metadata["waiter_poll_every_sec"].to_i
420
+ interval = 600 if interval <= 0
421
+ (now - last_seen) <= (interval * 2)
422
+ end
423
+
424
+ def parse_time(raw)
425
+ return raw if raw.is_a?(Time)
426
+ return nil if raw.nil?
427
+
428
+ Time.parse(raw.to_s)
429
+ rescue StandardError
430
+ nil
431
+ end
432
+
433
+ def normalize_poll_seconds(value)
434
+ parsed = value.to_i
435
+ return 600 if parsed <= 0
436
+
437
+ parsed
438
+ end
439
+
440
+ def normalize_timeout_seconds(value)
441
+ parsed = value.to_i
442
+ return 14_400 if parsed <= 0
443
+
444
+ parsed
445
+ end
446
+
447
+ def append_resume_note(ref, note, scope: nil)
448
+ current = show(ref, scope: scope)
449
+ return unless current
450
+
451
+ event = current[:event]
452
+ content = File.read(event.file_path)
453
+ frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
454
+ updated_body = body.to_s.sub(/\s*\z/, "")
455
+ updated_body << "\n\n## Resume Dispatch\n\n#{note}\n"
456
+ rebuilt = Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, updated_body)
457
+ File.write(event.file_path, rebuilt)
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Hitl
5
+ VERSION = "0.8.0"
6
+ end
7
+ end
data/lib/ace/hitl.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/items"
4
+ require_relative "hitl/version"
5
+ require_relative "hitl/atoms/hitl_file_pattern"
6
+ require_relative "hitl/atoms/hitl_id_formatter"
7
+ require_relative "hitl/models/hitl_event"
8
+ require_relative "hitl/molecules/hitl_config_loader"
9
+ require_relative "hitl/molecules/hitl_scanner"
10
+ require_relative "hitl/molecules/hitl_resolver"
11
+ require_relative "hitl/molecules/hitl_loader"
12
+ require_relative "hitl/molecules/hitl_display_formatter"
13
+ require_relative "hitl/molecules/hitl_answer_editor"
14
+ require_relative "hitl/molecules/hitl_creator"
15
+ require_relative "hitl/molecules/resume_dispatcher"
16
+ require_relative "hitl/molecules/worktree_scope_resolver"
17
+ require_relative "hitl/organisms/hitl_manager"
18
+ require_relative "hitl/cli"
19
+
20
+ module Ace
21
+ module Hitl
22
+ class Error < StandardError; end
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ace-hitl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Michal Czyz
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-04-05 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ace-support-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.29'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.29'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ace-support-config
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.9'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.9'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ace-support-fs
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ace-support-items
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.15'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.15'
68
+ - !ruby/object:Gem::Dependency
69
+ name: ace-b36ts
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.13'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.13'
82
+ - !ruby/object:Gem::Dependency
83
+ name: ace-support-cli
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.6'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.6'
96
+ description: Provides the dedicated ace-hitl package surface for HITL semantics and
97
+ CLI workflows.
98
+ email:
99
+ - mc@cs3b.com
100
+ executables:
101
+ - ace-hitl
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".ace-defaults/hitl/config.yml"
106
+ - ".ace-defaults/nav/protocols/skill-sources/ace-hitl.yml"
107
+ - ".ace-defaults/nav/protocols/wfi-sources/ace-hitl.yml"
108
+ - CHANGELOG.md
109
+ - README.md
110
+ - Rakefile
111
+ - docs/demo/ace-hitl-scope-and-answer.tape.yml
112
+ - docs/demo/fixtures/demo-context.md
113
+ - docs/usage.md
114
+ - exe/ace-hitl
115
+ - handbook/README.md
116
+ - handbook/skills/as-hitl/SKILL.md
117
+ - handbook/workflow-instructions/hitl.wf.md
118
+ - lib/ace/hitl.rb
119
+ - lib/ace/hitl/atoms/hitl_file_pattern.rb
120
+ - lib/ace/hitl/atoms/hitl_id_formatter.rb
121
+ - lib/ace/hitl/cli.rb
122
+ - lib/ace/hitl/cli/commands/create.rb
123
+ - lib/ace/hitl/cli/commands/list.rb
124
+ - lib/ace/hitl/cli/commands/show.rb
125
+ - lib/ace/hitl/cli/commands/update.rb
126
+ - lib/ace/hitl/cli/commands/wait.rb
127
+ - lib/ace/hitl/models/hitl_event.rb
128
+ - lib/ace/hitl/molecules/hitl_answer_editor.rb
129
+ - lib/ace/hitl/molecules/hitl_config_loader.rb
130
+ - lib/ace/hitl/molecules/hitl_creator.rb
131
+ - lib/ace/hitl/molecules/hitl_display_formatter.rb
132
+ - lib/ace/hitl/molecules/hitl_loader.rb
133
+ - lib/ace/hitl/molecules/hitl_resolver.rb
134
+ - lib/ace/hitl/molecules/hitl_scanner.rb
135
+ - lib/ace/hitl/molecules/resume_dispatcher.rb
136
+ - lib/ace/hitl/molecules/worktree_scope_resolver.rb
137
+ - lib/ace/hitl/organisms/hitl_manager.rb
138
+ - lib/ace/hitl/version.rb
139
+ homepage: https://github.com/cs3b/ace
140
+ licenses:
141
+ - MIT
142
+ metadata:
143
+ allowed_push_host: https://rubygems.org
144
+ homepage_uri: https://github.com/cs3b/ace
145
+ source_code_uri: https://github.com/cs3b/ace/tree/main/ace-hitl/
146
+ changelog_uri: https://github.com/cs3b/ace/blob/main/ace-hitl/CHANGELOG.md
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: 3.2.0
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubygems_version: 3.6.9
162
+ specification_version: 4
163
+ summary: Human-in-the-loop workflow package for ACE
164
+ test_files: []