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.
- checksums.yaml +7 -0
- data/.ace-defaults/hitl/config.yml +3 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-hitl.yml +13 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-hitl.yml +13 -0
- data/CHANGELOG.md +128 -0
- data/README.md +42 -0
- data/Rakefile +10 -0
- data/docs/demo/ace-hitl-scope-and-answer.tape.yml +37 -0
- data/docs/demo/fixtures/demo-context.md +7 -0
- data/docs/usage.md +112 -0
- data/exe/ace-hitl +18 -0
- data/handbook/README.md +14 -0
- data/handbook/skills/as-hitl/SKILL.md +29 -0
- data/handbook/workflow-instructions/hitl.wf.md +110 -0
- data/lib/ace/hitl/atoms/hitl_file_pattern.rb +20 -0
- data/lib/ace/hitl/atoms/hitl_id_formatter.rb +15 -0
- data/lib/ace/hitl/cli/commands/create.rb +76 -0
- data/lib/ace/hitl/cli/commands/list.rb +63 -0
- data/lib/ace/hitl/cli/commands/show.rb +79 -0
- data/lib/ace/hitl/cli/commands/update.rb +127 -0
- data/lib/ace/hitl/cli/commands/wait.rb +74 -0
- data/lib/ace/hitl/cli.rb +62 -0
- data/lib/ace/hitl/models/hitl_event.rb +32 -0
- data/lib/ace/hitl/molecules/hitl_answer_editor.rb +25 -0
- data/lib/ace/hitl/molecules/hitl_config_loader.rb +50 -0
- data/lib/ace/hitl/molecules/hitl_creator.rb +207 -0
- data/lib/ace/hitl/molecules/hitl_display_formatter.rb +53 -0
- data/lib/ace/hitl/molecules/hitl_loader.rb +113 -0
- data/lib/ace/hitl/molecules/hitl_resolver.rb +30 -0
- data/lib/ace/hitl/molecules/hitl_scanner.rb +42 -0
- data/lib/ace/hitl/molecules/resume_dispatcher.rb +99 -0
- data/lib/ace/hitl/molecules/worktree_scope_resolver.rb +77 -0
- data/lib/ace/hitl/organisms/hitl_manager.rb +462 -0
- data/lib/ace/hitl/version.rb +7 -0
- data/lib/ace/hitl.rb +24 -0
- metadata +164 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require_relative "../../molecules/hitl_display_formatter"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Hitl
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class List < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
desc "List HITL events"
|
|
14
|
+
|
|
15
|
+
option :status, type: :string, aliases: %w[-s], desc: "Filter by status"
|
|
16
|
+
option :kind, type: :string, aliases: %w[-k], desc: "Filter by kind"
|
|
17
|
+
option :tags, type: :string, aliases: %w[-T], desc: "Filter by tags (comma-separated)"
|
|
18
|
+
option :in, type: :string, aliases: %w[-i], desc: "Folder filter (next, all, archive)"
|
|
19
|
+
option :scope, type: :string, desc: "Scope (current, all)"
|
|
20
|
+
|
|
21
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
22
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
23
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
24
|
+
|
|
25
|
+
def call(**options)
|
|
26
|
+
scope = validate_scope(options[:scope])
|
|
27
|
+
status = options[:status]
|
|
28
|
+
kind = options[:kind]
|
|
29
|
+
tags = parse_tags(options[:tags])
|
|
30
|
+
in_folder = options[:in] || "next"
|
|
31
|
+
|
|
32
|
+
manager = Ace::Hitl::Organisms::HitlManager.new
|
|
33
|
+
events = manager.list(status: status, kind: kind, tags: tags, in_folder: in_folder, scope: scope)
|
|
34
|
+
|
|
35
|
+
puts Ace::Hitl::Molecules::HitlDisplayFormatter.format_list(
|
|
36
|
+
events,
|
|
37
|
+
total_count: manager.last_list_total,
|
|
38
|
+
global_folder_stats: manager.last_folder_counts
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse_tags(raw)
|
|
45
|
+
return [] unless raw
|
|
46
|
+
|
|
47
|
+
raw.split(",").map(&:strip).reject(&:empty?)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_scope(raw_scope)
|
|
51
|
+
return nil if raw_scope.nil?
|
|
52
|
+
|
|
53
|
+
scope = raw_scope.to_s.strip
|
|
54
|
+
allowed = Ace::Hitl::Molecules::WorktreeScopeResolver::VALID_SCOPES
|
|
55
|
+
return scope if allowed.include?(scope)
|
|
56
|
+
|
|
57
|
+
raise Ace::Support::Cli::Error.new("Invalid scope '#{raw_scope}'. Allowed: #{allowed.join(", ")}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Show < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "Show HITL event details"
|
|
13
|
+
|
|
14
|
+
argument :ref, required: true, desc: "HITL reference (full ID or shortcut)"
|
|
15
|
+
|
|
16
|
+
option :path, type: :boolean, desc: "Print file path only"
|
|
17
|
+
option :content, type: :boolean, desc: "Print raw markdown content"
|
|
18
|
+
option :scope, type: :string, desc: "Scope (current, all)"
|
|
19
|
+
|
|
20
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
21
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
22
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
23
|
+
|
|
24
|
+
def call(ref:, **options)
|
|
25
|
+
scope = validate_scope(options[:scope])
|
|
26
|
+
manager = Ace::Hitl::Organisms::HitlManager.new
|
|
27
|
+
begin
|
|
28
|
+
resolved = manager.show(ref, scope: scope)
|
|
29
|
+
rescue Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError => e
|
|
30
|
+
candidates = e.matches.map(&:file_path).join(", ")
|
|
31
|
+
raise Ace::Support::Cli::Error.new(
|
|
32
|
+
"HITL event '#{ref}' is ambiguous across scope '#{scope || "all"}'. Candidates: #{candidates}"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{ref}' not found") unless resolved
|
|
36
|
+
event = resolved[:event]
|
|
37
|
+
resolved_location = format_resolved_location(resolved)
|
|
38
|
+
|
|
39
|
+
if options[:path]
|
|
40
|
+
puts event.file_path
|
|
41
|
+
elsif options[:content]
|
|
42
|
+
puts resolved_location if resolved_location
|
|
43
|
+
puts File.read(event.file_path)
|
|
44
|
+
else
|
|
45
|
+
puts "ID: #{event.id}"
|
|
46
|
+
puts "Title: #{event.title}"
|
|
47
|
+
puts "Status: #{event.status}"
|
|
48
|
+
puts "Kind: #{event.kind}"
|
|
49
|
+
puts "Tags: #{event.tags.join(", ")}"
|
|
50
|
+
puts "Questions: #{event.questions.join(" | ")}"
|
|
51
|
+
puts "Answer: #{event.answer || "(none)"}"
|
|
52
|
+
puts resolved_location if resolved_location
|
|
53
|
+
puts "Path: #{event.file_path}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def validate_scope(raw_scope)
|
|
60
|
+
return nil if raw_scope.nil?
|
|
61
|
+
|
|
62
|
+
scope = raw_scope.to_s.strip
|
|
63
|
+
allowed = Ace::Hitl::Molecules::WorktreeScopeResolver::VALID_SCOPES
|
|
64
|
+
return scope if allowed.include?(scope)
|
|
65
|
+
|
|
66
|
+
raise Ace::Support::Cli::Error.new("Invalid scope '#{raw_scope}'. Allowed: #{allowed.join(", ")}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_resolved_location(resolved)
|
|
70
|
+
return nil unless resolved[:resolved_outside_current]
|
|
71
|
+
|
|
72
|
+
worktree = resolved[:resolved_worktree_root] || "(unknown worktree)"
|
|
73
|
+
"Resolved Location: worktree=#{worktree} path=#{resolved[:event].file_path}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/support/items"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Hitl
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Update < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
desc "Update HITL event metadata and/or answer"
|
|
14
|
+
|
|
15
|
+
argument :ref, required: true, desc: "HITL reference (full ID or shortcut)"
|
|
16
|
+
|
|
17
|
+
option :set, type: :string, repeat: true, desc: "Set field: key=value"
|
|
18
|
+
option :add, type: :string, repeat: true, desc: "Add field: key=value"
|
|
19
|
+
option :remove, type: :string, repeat: true, desc: "Remove field: key=value"
|
|
20
|
+
option :"move-to", type: :string, aliases: %w[-m], desc: "Move to folder (archive, next)"
|
|
21
|
+
option :answer, type: :string, desc: "Write ## Answer body and mark answered"
|
|
22
|
+
option :resume, type: :boolean, desc: "Dispatch resume after answer using waiter/session metadata"
|
|
23
|
+
option :scope, type: :string, desc: "Scope (current, all)"
|
|
24
|
+
|
|
25
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
26
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
27
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
28
|
+
|
|
29
|
+
def call(ref:, **options)
|
|
30
|
+
set_args = Array(options[:set])
|
|
31
|
+
add_args = Array(options[:add])
|
|
32
|
+
remove_args = Array(options[:remove])
|
|
33
|
+
move_to = options[:"move-to"]
|
|
34
|
+
answer = options[:answer]
|
|
35
|
+
resume = options[:resume] == true
|
|
36
|
+
scope = validate_scope(options[:scope])
|
|
37
|
+
|
|
38
|
+
if set_args.empty? && add_args.empty? && remove_args.empty? && move_to.nil? && answer.nil? && !resume
|
|
39
|
+
raise Ace::Support::Cli::Error.new("No update operations specified")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
set_hash = parse_kv_pairs(set_args)
|
|
43
|
+
add_hash = parse_kv_pairs(add_args)
|
|
44
|
+
remove_hash = parse_kv_pairs(remove_args)
|
|
45
|
+
|
|
46
|
+
manager = Ace::Hitl::Organisms::HitlManager.new
|
|
47
|
+
event = begin
|
|
48
|
+
manager.update(
|
|
49
|
+
ref,
|
|
50
|
+
set: set_hash,
|
|
51
|
+
add: add_hash,
|
|
52
|
+
remove: remove_hash,
|
|
53
|
+
move_to: move_to,
|
|
54
|
+
answer: answer,
|
|
55
|
+
scope: scope
|
|
56
|
+
)
|
|
57
|
+
rescue Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError => e
|
|
58
|
+
candidates = e.matches.map(&:file_path).join(", ")
|
|
59
|
+
raise Ace::Support::Cli::Error.new(
|
|
60
|
+
"HITL event '#{ref}' is ambiguous across scope '#{scope || "all"}'. Candidates: #{candidates}"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{ref}' not found") unless event
|
|
65
|
+
|
|
66
|
+
if move_to
|
|
67
|
+
folder_info = event.special_folder || "root"
|
|
68
|
+
puts "HITL event updated: #{event.id} #{event.title} -> #{folder_info}"
|
|
69
|
+
else
|
|
70
|
+
puts "HITL event updated: #{event.id} #{event.title}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
return unless resume
|
|
74
|
+
|
|
75
|
+
dispatch = manager.dispatch_resume(event.id, scope: scope)
|
|
76
|
+
case dispatch[:status]
|
|
77
|
+
when :waiter_active
|
|
78
|
+
puts "Resume dispatch skipped: active waiter lease detected."
|
|
79
|
+
when :dispatched
|
|
80
|
+
puts "Resume dispatched: mode=#{dispatch[:mode]} details=#{dispatch[:details]}"
|
|
81
|
+
puts "HITL event archived after successful dispatch."
|
|
82
|
+
when :no_answer
|
|
83
|
+
raise Ace::Support::Cli::Error.new("Cannot resume '#{event.id}' without an answer")
|
|
84
|
+
when :failed
|
|
85
|
+
raise Ace::Support::Cli::Error.new(dispatch[:error] || "Resume dispatch failed")
|
|
86
|
+
else
|
|
87
|
+
raise Ace::Support::Cli::Error.new("Resume dispatch failed: unexpected status #{dispatch[:status]}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def parse_kv_pairs(args)
|
|
94
|
+
result = {}
|
|
95
|
+
args.each do |arg|
|
|
96
|
+
unless arg.include?("=")
|
|
97
|
+
raise Ace::Support::Cli::Error.new("Invalid format '#{arg}': expected key=value")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
parsed = Ace::Support::Items::Atoms::FieldArgumentParser.parse([arg])
|
|
101
|
+
parsed.each do |key, value|
|
|
102
|
+
result[key] = if result.key?(key)
|
|
103
|
+
Array(result[key]) + Array(value)
|
|
104
|
+
else
|
|
105
|
+
value
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue Ace::Support::Items::Atoms::FieldArgumentParser::ParseError => e
|
|
109
|
+
raise Ace::Support::Cli::Error.new("Invalid argument '#{arg}': #{e.message}")
|
|
110
|
+
end
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_scope(raw_scope)
|
|
115
|
+
return nil if raw_scope.nil?
|
|
116
|
+
|
|
117
|
+
scope = raw_scope.to_s.strip
|
|
118
|
+
allowed = Ace::Hitl::Molecules::WorktreeScopeResolver::VALID_SCOPES
|
|
119
|
+
return scope if allowed.include?(scope)
|
|
120
|
+
|
|
121
|
+
raise Ace::Support::Cli::Error.new("Invalid scope '#{raw_scope}'. Allowed: #{allowed.join(", ")}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Wait < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "Wait for a specific HITL event answer (polling default)"
|
|
13
|
+
|
|
14
|
+
argument :ref, required: true, desc: "HITL reference (full ID or shortcut)"
|
|
15
|
+
|
|
16
|
+
option :"poll-every", type: :integer, desc: "Polling interval in seconds (default: 600)"
|
|
17
|
+
option :timeout, type: :integer, desc: "Max wait time in seconds (default: 14400)"
|
|
18
|
+
option :scope, type: :string, desc: "Scope (current, all)"
|
|
19
|
+
option :"session-id", type: :string, desc: "Waiter session id (optional)"
|
|
20
|
+
option :provider, type: :string, desc: "Waiter provider (optional)"
|
|
21
|
+
|
|
22
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
23
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
24
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
25
|
+
|
|
26
|
+
def call(ref:, **options)
|
|
27
|
+
scope = validate_scope(options[:scope])
|
|
28
|
+
poll_every = options[:"poll-every"] || 600
|
|
29
|
+
timeout = options[:timeout] || 14_400
|
|
30
|
+
|
|
31
|
+
waiter = {
|
|
32
|
+
session_id: options[:"session-id"] || ENV["ACE_AGENT_SESSION_ID"] || ENV["ACE_SESSION_ID"],
|
|
33
|
+
provider: options[:provider] || ENV["ACE_PROVIDER"]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
manager = Ace::Hitl::Organisms::HitlManager.new
|
|
37
|
+
result = manager.wait_for_answer(
|
|
38
|
+
ref,
|
|
39
|
+
scope: scope,
|
|
40
|
+
poll_every: poll_every,
|
|
41
|
+
timeout: timeout,
|
|
42
|
+
waiter: waiter
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
case result[:status]
|
|
46
|
+
when :answered
|
|
47
|
+
event = result[:event]
|
|
48
|
+
puts "HITL event answered: #{event.id} #{event.title}"
|
|
49
|
+
puts "Answer: #{event.answer}"
|
|
50
|
+
resume = event.metadata["resume_instructions"]
|
|
51
|
+
puts "Resume: #{resume}" if resume
|
|
52
|
+
when :timeout
|
|
53
|
+
raise Ace::Support::Cli::Error.new("Timed out waiting for HITL event '#{ref}'")
|
|
54
|
+
else
|
|
55
|
+
raise Ace::Support::Cli::Error.new("HITL event '#{ref}' not found")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def validate_scope(raw_scope)
|
|
62
|
+
return nil if raw_scope.nil?
|
|
63
|
+
|
|
64
|
+
scope = raw_scope.to_s.strip
|
|
65
|
+
allowed = Ace::Hitl::Molecules::WorktreeScopeResolver::VALID_SCOPES
|
|
66
|
+
return scope if allowed.include?(scope)
|
|
67
|
+
|
|
68
|
+
raise Ace::Support::Cli::Error.new("Invalid scope '#{raw_scope}'. Allowed: #{allowed.join(", ")}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/ace/hitl/cli.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require_relative "../hitl/version"
|
|
5
|
+
require_relative "cli/commands/create"
|
|
6
|
+
require_relative "cli/commands/show"
|
|
7
|
+
require_relative "cli/commands/list"
|
|
8
|
+
require_relative "cli/commands/update"
|
|
9
|
+
require_relative "cli/commands/wait"
|
|
10
|
+
|
|
11
|
+
module Ace
|
|
12
|
+
module Hitl
|
|
13
|
+
module HitlCLI
|
|
14
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
15
|
+
|
|
16
|
+
PROGRAM_NAME = "ace-hitl"
|
|
17
|
+
|
|
18
|
+
REGISTERED_COMMANDS = [
|
|
19
|
+
["create", "Create HITL event"],
|
|
20
|
+
["show", "Show HITL event details"],
|
|
21
|
+
["list", "List HITL events"],
|
|
22
|
+
["update", "Update HITL event metadata or answer"],
|
|
23
|
+
["wait", "Wait for an answer on a specific HITL event"]
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
HELP_EXAMPLES = [
|
|
27
|
+
"ace-hitl list --status pending",
|
|
28
|
+
"ace-hitl show abc123 --content",
|
|
29
|
+
"ace-hitl create \"Which auth strategy?\" --kind decision",
|
|
30
|
+
"ace-hitl update abc123 --answer \"Use JWT with refresh tokens\"",
|
|
31
|
+
"ace-hitl wait abc123 --poll-every 600 --timeout 14400"
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
register "create", CLI::Commands::Create
|
|
35
|
+
register "show", CLI::Commands::Show
|
|
36
|
+
register "list", CLI::Commands::List
|
|
37
|
+
register "update", CLI::Commands::Update
|
|
38
|
+
register "wait", CLI::Commands::Wait
|
|
39
|
+
|
|
40
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
41
|
+
gem_name: "ace-hitl",
|
|
42
|
+
version: Ace::Hitl::VERSION
|
|
43
|
+
)
|
|
44
|
+
register "version", version_cmd
|
|
45
|
+
register "--version", version_cmd
|
|
46
|
+
|
|
47
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
48
|
+
program_name: PROGRAM_NAME,
|
|
49
|
+
version: Ace::Hitl::VERSION,
|
|
50
|
+
commands: REGISTERED_COMMANDS,
|
|
51
|
+
examples: HELP_EXAMPLES
|
|
52
|
+
)
|
|
53
|
+
register "help", help_cmd
|
|
54
|
+
register "--help", help_cmd
|
|
55
|
+
register "-h", help_cmd
|
|
56
|
+
|
|
57
|
+
def self.start(args)
|
|
58
|
+
Ace::Support::Cli::Runner.new(self).call(args: args)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Hitl
|
|
5
|
+
module Models
|
|
6
|
+
HitlEvent = Struct.new(
|
|
7
|
+
:id,
|
|
8
|
+
:status,
|
|
9
|
+
:kind,
|
|
10
|
+
:title,
|
|
11
|
+
:tags,
|
|
12
|
+
:questions,
|
|
13
|
+
:answer,
|
|
14
|
+
:content,
|
|
15
|
+
:path,
|
|
16
|
+
:file_path,
|
|
17
|
+
:special_folder,
|
|
18
|
+
:created_at,
|
|
19
|
+
:metadata,
|
|
20
|
+
keyword_init: true
|
|
21
|
+
) do
|
|
22
|
+
def shortcut
|
|
23
|
+
id[-3..]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def answered?
|
|
27
|
+
status.to_s == "answered" || metadata["answered"] == true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Hitl
|
|
5
|
+
module Molecules
|
|
6
|
+
class HitlAnswerEditor
|
|
7
|
+
ANSWER_HEADER = /^## Answer\s*$/
|
|
8
|
+
|
|
9
|
+
def self.apply(body, answer)
|
|
10
|
+
answer_text = answer.to_s.strip
|
|
11
|
+
replacement = "## Answer\n\n#{answer_text}\n"
|
|
12
|
+
source = body.to_s.rstrip
|
|
13
|
+
|
|
14
|
+
if source.match?(ANSWER_HEADER)
|
|
15
|
+
source.sub(/^## Answer[ \t]*(?:\n.*)?\z/m, replacement)
|
|
16
|
+
elsif source.empty?
|
|
17
|
+
replacement
|
|
18
|
+
else
|
|
19
|
+
"#{source}\n\n#{replacement}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "ace/support/config"
|
|
5
|
+
require "ace/support/fs"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Hitl
|
|
9
|
+
module Molecules
|
|
10
|
+
class HitlConfigLoader
|
|
11
|
+
DEFAULT_ROOT_DIR = ".ace-local/hitl"
|
|
12
|
+
DEFAULT_KIND = "clarification"
|
|
13
|
+
|
|
14
|
+
def self.load(gem_root: nil)
|
|
15
|
+
gem_root ||= File.expand_path("../../../..", __dir__)
|
|
16
|
+
resolver = Ace::Support::Config.create(
|
|
17
|
+
config_dir: ".ace",
|
|
18
|
+
defaults_dir: ".ace-defaults",
|
|
19
|
+
gem_path: gem_root
|
|
20
|
+
)
|
|
21
|
+
{"hitl" => resolver.resolve_namespace("hitl").data}
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
warn "ace-hitl: Could not load config: #{e.class} - #{e.message}" if Ace::Hitl.respond_to?(:debug?) && Ace::Hitl.debug?
|
|
24
|
+
load_defaults_fallback(gem_root: gem_root)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.root_dir(config = nil)
|
|
28
|
+
config ||= load
|
|
29
|
+
dir = config.dig("hitl", "root_dir") || DEFAULT_ROOT_DIR
|
|
30
|
+
|
|
31
|
+
if dir.start_with?("/")
|
|
32
|
+
dir
|
|
33
|
+
else
|
|
34
|
+
File.join(Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current, dir)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.load_defaults_fallback(gem_root:)
|
|
39
|
+
defaults_path = File.join(gem_root, ".ace-defaults", "hitl", "config.yml")
|
|
40
|
+
return {} unless File.exist?(defaults_path)
|
|
41
|
+
|
|
42
|
+
YAML.safe_load_file(defaults_path, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
43
|
+
rescue StandardError
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
private_class_method :load_defaults_fallback
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|