ace-hitl 0.24.2 → 0.25.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 +4 -4
- data/CHANGELOG.md +61 -1
- data/docs/usage.md +103 -2
- data/lib/ace/hitl/cli/commands/apply.rb +43 -0
- data/lib/ace/hitl/cli/commands/ask.rb +10 -3
- data/lib/ace/hitl/cli/commands/close.rb +37 -0
- data/lib/ace/hitl/cli/commands/history.rb +60 -0
- data/lib/ace/hitl/cli/commands/lesson.rb +47 -0
- data/lib/ace/hitl/cli/commands/reply.rb +65 -0
- data/lib/ace/hitl/cli/commands/suggest.rb +47 -0
- data/lib/ace/hitl/cli/commands/sweep.rb +33 -0
- data/lib/ace/hitl/cli.rb +22 -1
- data/lib/ace/hitl/molecules/case_file_writer.rb +68 -0
- data/lib/ace/hitl/molecules/hitl_creator.rb +6 -1
- data/lib/ace/hitl/molecules/hitl_lesson_editor.rb +43 -0
- data/lib/ace/hitl/molecules/hitl_suggestion_editor.rb +48 -0
- data/lib/ace/hitl/organisms/hitl_manager.rb +583 -65
- data/lib/ace/hitl/organisms/hitl_sender.rb +6 -3
- data/lib/ace/hitl/version.rb +1 -1
- data/lib/ace/hitl.rb +2 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9115490bffe274fbc591fc0c0c3efb826c70fe549b17e7d57b5928e298ffe23
|
|
4
|
+
data.tar.gz: e14fa6ef9c4251fcad792f2c8cbdd362fe642b9c66dc7060fa621e6785f65d0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8f52aef4b90d868a62e1966523282ea1a74eabe20930034736e1a73a941c701df4c4580297659ea8c5255dc45676cf3f37b466c934faa5ce7d71e10cc2d4a5f
|
|
7
|
+
data.tar.gz: 202ca6825f02dcaec4da9fce88aaeef2ca66c33f98ef6cf51daa6585ec552dc7eeff8cfb3556ad35881aa0ccfa3cb13fc7653b68077a0ffbfcddbbf5b6e6f686
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,67 @@ All notable changes to `ace-hitl` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.25.0] - 2026-10-30
|
|
9
|
+
|
|
10
|
+
### Fixed (review rounds 3/3b + re-review R2)
|
|
11
|
+
|
|
12
|
+
- Case writes are crash-atomic under a stable sidecar lock (`<case>.lock`)
|
|
13
|
+
through the shared `Molecules::CaseFileWriter`: every writer path (reply,
|
|
14
|
+
suggestion, lesson, lifecycle, field updates, delivery persistence,
|
|
15
|
+
resume notes) holds the sidecar across tmp+fsync+rename — the
|
|
16
|
+
flock+rename lost-update race, the in-place `truncate(0)` field update
|
|
17
|
+
(a killed process could zero the case and lose the answer; SIGKILL
|
|
18
|
+
regression test added), and unlocked writer paths are closed. Terminal
|
|
19
|
+
cases (closed/cancelled) refuse new answers, suggestions and lessons;
|
|
20
|
+
history capture is section-scoped; `history --format table`/`--quiet`
|
|
21
|
+
are real. Case files are created 0600 and every tmp write
|
|
22
|
+
is 0600 before rename; corrupt-file parses refuse with a named error
|
|
23
|
+
(Psych::Exception aligned across paths).
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `ace-hitl lesson` + `HitlManager#add_lesson` and `ace-hitl history` +
|
|
28
|
+
`#decision_history` (task 8xs.t.pu0.c): learning from real decisions — one
|
|
29
|
+
Lesson section per case citing exact question/suggestion/decision
|
|
30
|
+
revisions (idempotent, never overwriting history, requiring a suggestion
|
|
31
|
+
to compare against), and a citable, scope-local, secret-free decision
|
|
32
|
+
history for the next suggestion.
|
|
33
|
+
|
|
34
|
+
- `ace-hitl suggest` and `HitlManager#add_suggestion` (task 8xs.t.pu0.b): the
|
|
35
|
+
Second Captain's marked advisory proposal — own section before the Answer
|
|
36
|
+
(survives answer rewrites), author/rationale/grounds recorded, one per case
|
|
37
|
+
(identical repeat = duplicate), refused on answered or secret cases, never
|
|
38
|
+
an answer or approval.
|
|
39
|
+
|
|
40
|
+
- `ace-hitl ask --secret` (task 8xs.t.pu0.9): the expected answer (e.g. an
|
|
41
|
+
OTP) is marked secret AT CREATION — no title heuristics; the case carries
|
|
42
|
+
`secret: true` and the armed wake request flags the bridge to wake with the
|
|
43
|
+
case reference only, never the value.
|
|
44
|
+
|
|
45
|
+
- Case lifecycle (task 8xs.t.pu0.6): `ace-hitl apply CASE --ref OWNER_REF`
|
|
46
|
+
records the applied effect with its owner reference (idempotent per
|
|
47
|
+
reference, conflicting references rejected), `ace-hitl close CASE` is
|
|
48
|
+
terminal only after the applied proof (duplicate no-op, closed cases never
|
|
49
|
+
re-open), `cancel_case` terminates without application, and
|
|
50
|
+
`ace-hitl sweep [--apply]` lists/removes ONLY the ask-wake request
|
|
51
|
+
leftovers of terminally closed/cancelled cases — active cases, unknown
|
|
52
|
+
ids and the canonical decision history are always preserved. Lifecycle
|
|
53
|
+
state lives in the canonical case file (receipt pending/received,
|
|
54
|
+
application pending/applied/closed/cancelled); transitions are locked and
|
|
55
|
+
conflict-rejecting.
|
|
56
|
+
|
|
57
|
+
- `ace-hitl reply CASE_ID --stdin` (task 8xs.t.pu0.5): receiver-side answer
|
|
58
|
+
delivery on the owner's machine. Output carries only the case and its state
|
|
59
|
+
(`received`/`duplicate`); the reference must be the exact case id (no
|
|
60
|
+
shortcut/latest resolution), identical redelivery is idempotent without a
|
|
61
|
+
rewrite, a conflicting answer never overwrites the decision, and empty
|
|
62
|
+
answers/reports/unknown references fail as explicit non-deliveries.
|
|
63
|
+
|
|
64
|
+
## [0.24.3] - 2026-10-29
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Answer and field-update rebuilds parse the case file strictly: an unparsable frontmatter block aborts the mutation instead of silently rebuilding the case from an empty frontmatter (HOTFIX cycle-2b follow-up to the 8xsdfo ask-flow corruption).
|
|
9
69
|
|
|
10
70
|
## [0.24.2] - 2026-10-27
|
|
11
71
|
|
data/docs/usage.md
CHANGED
|
@@ -326,8 +326,13 @@ HITL event 8xhkep sent through 1 route(s):
|
|
|
326
326
|
8xhkep
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
-
The final stdout line is always the bare case id.
|
|
330
|
-
|
|
329
|
+
The final stdout line is always the bare case id. `--secret` marks the
|
|
330
|
+
EXPECTED answer as a secret (e.g. an OTP) at creation — the armed wake then
|
|
331
|
+
carries the CASE REFERENCE ONLY (the value never passes through the model;
|
|
332
|
+
the executor reads it from the case file through its secure channel), and a
|
|
333
|
+
rejected or expired secret answer needs a NEW case, never a replay. ask
|
|
334
|
+
never blocks and never polls, carries no urgency language, and has no
|
|
335
|
+
asker-side timeouts — the
|
|
331
336
|
Captain replies whenever he wants. The answer is delivered TO the agent as
|
|
332
337
|
an ace-wake wake carrying the answer content (see the ace-wake README's ask
|
|
333
338
|
bridge contract); on wake, the agent acts immediately.
|
|
@@ -339,6 +344,102 @@ asynchronous). Exit 1 only for genuine rejects (a route refusing the
|
|
|
339
344
|
frame, or a concrete configuration error); no wake request is dropped for
|
|
340
345
|
a rejected ask.
|
|
341
346
|
|
|
347
|
+
## Reply
|
|
348
|
+
|
|
349
|
+
`ace-hitl reply CASE_ID --stdin` (task 8xs.t.pu0.5) is the receiver-side
|
|
350
|
+
half of answer delivery: the transport (Hermes) runs it ON THE OWNER'S
|
|
351
|
+
MACHINE, in the owner's directory, with the answer text on stdin — never
|
|
352
|
+
as a shell command or an argv element:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
echo "Proceed with batch 2" | ace-hitl reply 8xhkep --stdin
|
|
356
|
+
{"case":"8xhkep","state":"received"}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
- The output names ONLY the case and its state: `received`, or `duplicate`
|
|
360
|
+
when the identical answer is redelivered (idempotent — the case file is
|
|
361
|
+
never rewritten). Success means ace-hitl confirmed the DURABLE write; a
|
|
362
|
+
reply frame sitting in a transport is not delivery.
|
|
363
|
+
- The reference must be the case's EXACT id from the ask — no shortcut or
|
|
364
|
+
latest-match resolution, so a reply can never land in a foreign case on
|
|
365
|
+
collision.
|
|
366
|
+
- Every failure is an explicit non-delivery with no side effects: an empty
|
|
367
|
+
answer, a report instead of a question, a conflicting (different)
|
|
368
|
+
answer — a reply never overwrites a human decision — or an unknown
|
|
369
|
+
reference all exit 1 without writing.
|
|
370
|
+
- The receiver does not need to be running: the answered case file is the
|
|
371
|
+
durable record; the armed ask wake consumes the change whenever the
|
|
372
|
+
asking session returns.
|
|
373
|
+
|
|
374
|
+
## Suggest (Second Captain)
|
|
375
|
+
|
|
376
|
+
`ace-hitl suggest CASE_ID --by AUTHOR --text ... --rationale ... [--refs a,b]`
|
|
377
|
+
(task 8xs.t.pu0.b): the advisory Second Captain's MARKED proposal — its own
|
|
378
|
+
`## Suggestion` section in the exact case, with author, timestamp, grounds,
|
|
379
|
+
proposal and rationale, inserted BEFORE the `## Answer` section so an answer
|
|
380
|
+
rewrite can never remove it. It is not an Answer, never satisfies a gate,
|
|
381
|
+
and never acts in the human's name: the human decision always outranks it.
|
|
382
|
+
|
|
383
|
+
- One proposal per case: an identical repeat reports `duplicate` (no-op); a
|
|
384
|
+
different second proposal is refused.
|
|
385
|
+
- A case the human already answered refuses the suggestion ("discard the
|
|
386
|
+
late suggestion") without writing; a case marked `secret` (OTP) is never
|
|
387
|
+
delegated to the advisor.
|
|
388
|
+
- Shortcuts/unknown ids are refused exactly like `reply`.
|
|
389
|
+
|
|
390
|
+
## Lesson & History (Second Captain learning)
|
|
391
|
+
|
|
392
|
+
`ace-hitl lesson CASE_ID --by AGENT --relation accepted|rejected|modified
|
|
393
|
+
--takeaway ...` (task 8xs.t.pu0.c): after the human answers a case that
|
|
394
|
+
carried a suggestion, the advisory agent writes ONE `## Lesson` section at
|
|
395
|
+
the same case — its comparison takeaway with references to the exact
|
|
396
|
+
question/suggestion/decision revisions, its own author and time. The lesson
|
|
397
|
+
never overwrites the question, suggestion or decision, never changes the
|
|
398
|
+
answer's author, and reprocessing is idempotent (an identical repeat is a
|
|
399
|
+
`duplicate`; a second different lesson is refused). A case without a
|
|
400
|
+
suggestion has nothing to compare — refused, never a fabricated agreement.
|
|
401
|
+
|
|
402
|
+
`ace-hitl history [--scope current] [--limit N]`: the citable decision
|
|
403
|
+
history for the NEXT suggestion — answered, NON-SECRET question cases of
|
|
404
|
+
this scope, each row with its checkable references (case id, suggestion,
|
|
405
|
+
decision, lesson+relation). Secret/OTP cases are filtered out before
|
|
406
|
+
anything reaches a model; there is no embedding store, decision index, or
|
|
407
|
+
corpus copy — HITL stays the single source, and the wider lab overseer's
|
|
408
|
+
access is not a license to share other projects' history.
|
|
409
|
+
|
|
410
|
+
## Case lifecycle
|
|
411
|
+
|
|
412
|
+
The canonical case file carries the whole lifecycle (task 8xs.t.pu0.6) —
|
|
413
|
+
there is no second registry:
|
|
414
|
+
|
|
415
|
+
- receipt: `pending` → `received` (the answer was durably delivered);
|
|
416
|
+
- application: `pending` → `applied` → `closed`, or `cancelled` (terminal
|
|
417
|
+
without application). Reports stay `recorded` — they never await an
|
|
418
|
+
answer or application.
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
ace-hitl apply 8xhkep --ref pr:398 # the effect owner reference verified by the consumer
|
|
422
|
+
ace-hitl close 8xhkep # terminal, only after the applied proof
|
|
423
|
+
ace-hitl sweep # list transport leftovers of terminal cases
|
|
424
|
+
ace-hitl sweep --apply # remove ONLY the proven leftovers
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Exactly-once belongs to the EFFECT, not the notification count: after a
|
|
428
|
+
crash between effect and `applied`, the consumer first re-checks the effect
|
|
429
|
+
at its owner (PR/release/assignment); an undecidable outcome is an explicit
|
|
430
|
+
reconciliation block, never a blind retry. Re-applying the same effect
|
|
431
|
+
reference is a no-op (`duplicate`); a different reference for an applied
|
|
432
|
+
case is a conflict. A duplicate close is a no-op. A late different answer
|
|
433
|
+
was already rejected at delivery, and a closed case never re-opens — an
|
|
434
|
+
explicit decision change needs a NEW case.
|
|
435
|
+
|
|
436
|
+
`ace-hitl sweep` qualifies the ask-wake request files against the canonical
|
|
437
|
+
cases: deletion candidates are ONLY requests whose case is terminally
|
|
438
|
+
`closed` or `cancelled`. Active cases (pending/received/applied), unknown
|
|
439
|
+
ids, unparseable requests and the canonical decision history (including the
|
|
440
|
+
history Second Captain learns from) are always preserved; without `--apply`
|
|
441
|
+
the command only lists.
|
|
442
|
+
|
|
342
443
|
## Lifecycle event names
|
|
343
444
|
|
|
344
445
|
HITL lifecycle signals use this namespace:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl apply CASE --ref OWNER_REF` (task 8xs.t.pu0.6):
|
|
10
|
+
# record that the answer's effect was applied, WITH the
|
|
11
|
+
# reference to the effect owner (task/assignment/PR) the
|
|
12
|
+
# consumer verified first. Exactly-once belongs to the effect:
|
|
13
|
+
# after a crash the consumer re-checks the effect at its owner
|
|
14
|
+
# before calling this; an undecidable outcome is an explicit
|
|
15
|
+
# reconciliation block, never a blind retry. Close happens only
|
|
16
|
+
# after this proof exists.
|
|
17
|
+
class Apply < Ace::Support::Cli::Command
|
|
18
|
+
include Ace::Support::Cli::Base
|
|
19
|
+
|
|
20
|
+
desc "Record the answer's applied effect with its owner reference"
|
|
21
|
+
|
|
22
|
+
example ['ace-hitl apply 8xhkep --ref pr:398']
|
|
23
|
+
|
|
24
|
+
argument :case_id, required: true, desc: "Exact HITL case id"
|
|
25
|
+
|
|
26
|
+
option :ref, type: :string, required: true,
|
|
27
|
+
desc: "Effect owner reference (task/assignment/PR) verified by the consumer"
|
|
28
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
29
|
+
|
|
30
|
+
def call(case_id:, ref:, **)
|
|
31
|
+
state = Ace::Hitl::Organisms::HitlManager.new.apply_answer_effect(case_id, ref)
|
|
32
|
+
puts JSON.generate({"case" => case_id, "state" => state.to_s})
|
|
33
|
+
rescue Ace::Hitl::Organisms::HitlManager::UnknownCaseError,
|
|
34
|
+
Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError,
|
|
35
|
+
Ace::Hitl::Organisms::HitlManager::NotAnswerableError,
|
|
36
|
+
Ace::Hitl::Organisms::HitlManager::LifecycleTransitionError => e
|
|
37
|
+
raise Ace::Support::Cli::Error.new("not applied: #{e.message}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -36,6 +36,8 @@ module Ace
|
|
|
36
36
|
option :question, type: :string, repeat: true, aliases: %w[-Q], desc: "Question line (repeatable)"
|
|
37
37
|
option :task, type: :string, desc: "Task reference"
|
|
38
38
|
option :tags, type: :string, aliases: %w[-T], desc: "Comma-separated tags"
|
|
39
|
+
option :secret, type: :boolean, default: false,
|
|
40
|
+
desc: "The expected answer is a secret (e.g. OTP): the wake carries the case reference only, never the value"
|
|
39
41
|
|
|
40
42
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
41
43
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
@@ -78,7 +80,8 @@ module Ace
|
|
|
78
80
|
type: "question",
|
|
79
81
|
task: options[:task],
|
|
80
82
|
questions: Array(options[:question]).map(&:strip).reject(&:empty?),
|
|
81
|
-
tags: parse_tags(options[:tags])
|
|
83
|
+
tags: parse_tags(options[:tags]),
|
|
84
|
+
secret: options[:secret] == true
|
|
82
85
|
)
|
|
83
86
|
end
|
|
84
87
|
|
|
@@ -111,8 +114,11 @@ module Ace
|
|
|
111
114
|
|
|
112
115
|
# Drop the wake request the ace-wake extension consumes through its
|
|
113
116
|
# own reconcile pass and turns into a scoped watch on the case file:
|
|
114
|
-
# when the answer lands, the wake carries the answer content
|
|
115
|
-
#
|
|
117
|
+
# when the answer lands, the wake carries the answer content —
|
|
118
|
+
# EXCEPT for a secret ask (8xs.t.pu0.9), where the wake carries the
|
|
119
|
+
# case reference only and the executor reads the value from the
|
|
120
|
+
# case file through its secure channel. The atomic replace keeps
|
|
121
|
+
# the consumer from reading a torn request.
|
|
116
122
|
def arm_wake(event, manager)
|
|
117
123
|
requests_dir = File.join(File.expand_path(manager.root_dir), "wake-requests")
|
|
118
124
|
FileUtils.mkdir_p(requests_dir)
|
|
@@ -122,6 +128,7 @@ module Ace
|
|
|
122
128
|
"case_id" => event.id,
|
|
123
129
|
"case_path" => File.expand_path(event.file_path)
|
|
124
130
|
}
|
|
131
|
+
request["secret"] = true if event.metadata["secret"] == true
|
|
125
132
|
temporary = File.join(requests_dir, ".#{event.id}-#{SecureRandom.hex(8)}.tmp")
|
|
126
133
|
final = File.join(requests_dir, "#{event.id}.json")
|
|
127
134
|
File.write(temporary, JSON.generate(request))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl close CASE` (task 8xs.t.pu0.6): terminal close, only
|
|
10
|
+
# after the proven applied effect. A duplicate close is a no-op;
|
|
11
|
+
# a late different answer was already rejected at delivery and a
|
|
12
|
+
# closed case never re-opens — an explicit decision change needs
|
|
13
|
+
# a NEW case.
|
|
14
|
+
class Close < Ace::Support::Cli::Command
|
|
15
|
+
include Ace::Support::Cli::Base
|
|
16
|
+
|
|
17
|
+
desc "Close a case terminally after its applied effect"
|
|
18
|
+
|
|
19
|
+
example ['ace-hitl close 8xhkep']
|
|
20
|
+
|
|
21
|
+
argument :case_id, required: true, desc: "Exact HITL case id"
|
|
22
|
+
|
|
23
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
24
|
+
|
|
25
|
+
def call(case_id:, **)
|
|
26
|
+
state = Ace::Hitl::Organisms::HitlManager.new.close_case(case_id)
|
|
27
|
+
puts JSON.generate({"case" => case_id, "state" => state.to_s})
|
|
28
|
+
rescue Ace::Hitl::Organisms::HitlManager::UnknownCaseError,
|
|
29
|
+
Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError,
|
|
30
|
+
Ace::Hitl::Organisms::HitlManager::LifecycleTransitionError => e
|
|
31
|
+
raise Ace::Support::Cli::Error.new("not closed: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl history` (task 8xs.t.pu0.c): the citable decision
|
|
10
|
+
# history for the NEXT Second Captain suggestion — answered,
|
|
11
|
+
# NON-SECRET question cases of THIS project scope, each row with
|
|
12
|
+
# its checkable references (case id, suggestion, decision,
|
|
13
|
+
# lesson). Secret/OTP cases never reach the model; no wider
|
|
14
|
+
# lab-wide sharing. No embedding store or decision index: HITL
|
|
15
|
+
# stays the single source.
|
|
16
|
+
class History < Ace::Support::Cli::Command
|
|
17
|
+
include Ace::Support::Cli::Base
|
|
18
|
+
|
|
19
|
+
desc "List the citable decision history (answered, non-secret cases of this scope)"
|
|
20
|
+
|
|
21
|
+
option :scope, type: :string, default: "current", desc: "current or all linked worktrees"
|
|
22
|
+
option :limit, type: :integer, default: 50, desc: "Maximum rows"
|
|
23
|
+
option :format, default: "json", desc: "Output format (json, table)"
|
|
24
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
25
|
+
|
|
26
|
+
def call(scope: "current", limit: 50, format: "json", quiet: false, **)
|
|
27
|
+
rows = Ace::Hitl::Organisms::HitlManager.new
|
|
28
|
+
.decision_history(scope: scope, limit: limit)
|
|
29
|
+
return if quiet
|
|
30
|
+
|
|
31
|
+
if format == "table"
|
|
32
|
+
puts render_table(rows)
|
|
33
|
+
else
|
|
34
|
+
puts JSON.pretty_generate({"history" => rows, "count" => rows.size})
|
|
35
|
+
end
|
|
36
|
+
rescue ArgumentError => e
|
|
37
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# One row per decided case: the case id, the decision (truncated
|
|
43
|
+
# to one line for the terminal) and the citable lesson relation.
|
|
44
|
+
def render_table(rows)
|
|
45
|
+
return "no decided cases in this scope" if rows.empty?
|
|
46
|
+
|
|
47
|
+
lines = ["CASE DECISION LESSON"]
|
|
48
|
+
rows.each do |row|
|
|
49
|
+
decision = row["decision"].to_s.gsub(/\s+/, " ")
|
|
50
|
+
decision = decision.length > 44 ? "#{decision[0, 43]}…" : decision
|
|
51
|
+
lesson = row["lesson_relation"].to_s.empty? ? "-" : row["lesson_relation"].to_s
|
|
52
|
+
lines << format("%-18s %-44s %s", row["id"], decision, lesson)
|
|
53
|
+
end
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl lesson CASE_ID --by AGENT --relation accepted|rejected|
|
|
10
|
+
# modified --takeaway ...` (task 8xs.t.pu0.c): the advisory
|
|
11
|
+
# agent's takeaway from comparing the accepted human decision
|
|
12
|
+
# with its own suggestion — one `## Lesson` section at the same
|
|
13
|
+
# case, referencing the exact question/suggestion/decision
|
|
14
|
+
# revisions. Never a new decision; never changes the answer's
|
|
15
|
+
# author; identical repeats are no-ops.
|
|
16
|
+
class Lesson < Ace::Support::Cli::Command
|
|
17
|
+
include Ace::Support::Cli::Base
|
|
18
|
+
|
|
19
|
+
desc "Write the lesson drawn from the human decision vs the suggestion"
|
|
20
|
+
|
|
21
|
+
example ['ace-hitl lesson 8xhkep --by second-captain --relation rejected ',
|
|
22
|
+
' --takeaway "Captain prioritized rollout safety over speed"']
|
|
23
|
+
|
|
24
|
+
argument :case_id, required: true, desc: "Exact HITL case id"
|
|
25
|
+
|
|
26
|
+
option :by, type: :string, required: true, desc: "Advisory agent author identity"
|
|
27
|
+
option :relation, type: :string, required: true,
|
|
28
|
+
desc: "accepted | rejected | modified (decision vs suggestion)"
|
|
29
|
+
option :takeaway, type: :string, required: true, desc: "What the next suggestion should learn"
|
|
30
|
+
option :refs, type: :string, desc: "Comma-separated extra references"
|
|
31
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
32
|
+
|
|
33
|
+
def call(case_id:, by:, relation:, takeaway:, refs: nil, **)
|
|
34
|
+
state = Ace::Hitl::Organisms::HitlManager.new
|
|
35
|
+
.add_lesson(case_id, by: by, relation: relation, takeaway: takeaway,
|
|
36
|
+
refs: refs.to_s.split(",").map(&:strip).reject(&:empty?))
|
|
37
|
+
puts JSON.generate({"case" => case_id, "lesson" => state.to_s})
|
|
38
|
+
rescue Ace::Hitl::Organisms::HitlManager::UnknownCaseError,
|
|
39
|
+
Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError,
|
|
40
|
+
Ace::Hitl::Organisms::HitlManager::LessonRefusedError => e
|
|
41
|
+
raise Ace::Support::Cli::Error.new("lesson refused: #{e.message}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl reply CASE_ID --stdin` (task 8xs.t.pu0.5): the
|
|
10
|
+
# receiver-side half of HITL answer delivery. Hermes runs this
|
|
11
|
+
# command on the OWNER's machine, in the owner's directory, with
|
|
12
|
+
# a structured payload on stdin — the answer text never becomes
|
|
13
|
+
# a shell command or an argv element. The output names ONLY the
|
|
14
|
+
# case and its state: `{"case": ..., "state": "received"}` (a
|
|
15
|
+
# redelivered identical answer reports `"duplicate"`). Success
|
|
16
|
+
# means ace-hitl CONFIRMED the durable write — a reply frame
|
|
17
|
+
# sitting in a transport is not delivery.
|
|
18
|
+
#
|
|
19
|
+
# Every failure is an explicit non-delivery with no side effects
|
|
20
|
+
# on any other case: an empty answer, a report instead of a
|
|
21
|
+
# question, a conflicting answer, or a reference that is not the
|
|
22
|
+
# EXACT case id (no shortcut or latest resolution) all fail
|
|
23
|
+
# without writing.
|
|
24
|
+
class Reply < Ace::Support::Cli::Command
|
|
25
|
+
include Ace::Support::Cli::Base
|
|
26
|
+
|
|
27
|
+
desc "Receive a delivered human answer for one exact case; answer on stdin"
|
|
28
|
+
|
|
29
|
+
example [
|
|
30
|
+
'echo "Proceed with batch 2" | ace-hitl reply 8xtab1cd --stdin'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
argument :case_id, required: true, desc: "Exact HITL case id from the ask"
|
|
34
|
+
|
|
35
|
+
option :stdin, type: :boolean, required: true, desc: "Read the answer text from stdin"
|
|
36
|
+
|
|
37
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
38
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
39
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
40
|
+
|
|
41
|
+
def call(case_id:, stdin: nil, **options)
|
|
42
|
+
unless stdin
|
|
43
|
+
raise Ace::Support::Cli::Error.new("reply reads the answer from stdin; pass --stdin")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
answer = $stdin.read.to_s
|
|
47
|
+
if answer.strip.empty?
|
|
48
|
+
raise Ace::Support::Cli::Error.new("empty answer; nothing was written")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
state = Ace::Hitl::Organisms::HitlManager.new.receive_answer(case_id, answer)
|
|
52
|
+
# Only the case and the outcome — never the answer text.
|
|
53
|
+
puts JSON.generate({"case" => case_id, "state" => state.to_s})
|
|
54
|
+
rescue Ace::Hitl::Organisms::HitlManager::UnknownCaseError,
|
|
55
|
+
Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError,
|
|
56
|
+
Ace::Hitl::Organisms::HitlManager::NotAnswerableError,
|
|
57
|
+
Ace::Hitl::Organisms::HitlManager::AnswerConflictError,
|
|
58
|
+
Ace::Hitl::Organisms::HitlManager::TerminalCaseError => e
|
|
59
|
+
raise Ace::Support::Cli::Error.new("not delivered: #{e.message}")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl suggest CASE_ID --by AUTHOR --text ... --rationale ...`
|
|
10
|
+
# (task 8xs.t.pu0.b): the Second Captain's ADVISORY proposal —
|
|
11
|
+
# one marked section in the exact case, with author, rationale
|
|
12
|
+
# and grounds. Never an Answer, never a human approval, never a
|
|
13
|
+
# gate release. Identical repeats are no-ops; a second different
|
|
14
|
+
# proposal, a secret case, or a case the human already answered
|
|
15
|
+
# are explicit refusals without side effects.
|
|
16
|
+
class Suggest < Ace::Support::Cli::Command
|
|
17
|
+
include Ace::Support::Cli::Base
|
|
18
|
+
|
|
19
|
+
desc "Write the Second Captain's marked advisory suggestion into one exact case"
|
|
20
|
+
|
|
21
|
+
example ['ace-hitl suggest 8xhkep --by second-captain --text "Proceed" ',
|
|
22
|
+
' --rationale "tests green, review accept at same head" --refs pr:398']
|
|
23
|
+
|
|
24
|
+
argument :case_id, required: true, desc: "Exact HITL case id"
|
|
25
|
+
|
|
26
|
+
option :by, type: :string, required: true, desc: "Advisory author identity"
|
|
27
|
+
option :text, type: :string, required: true, desc: "Proposed answer text"
|
|
28
|
+
option :rationale, type: :string, required: true, desc: "Why this proposal is defensible"
|
|
29
|
+
option :refs, type: :string, desc: "Comma-separated grounds references"
|
|
30
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
31
|
+
|
|
32
|
+
def call(case_id:, by:, text:, rationale:, refs: nil, **)
|
|
33
|
+
state = Ace::Hitl::Organisms::HitlManager.new
|
|
34
|
+
.add_suggestion(case_id, author: by, text: text, rationale: rationale,
|
|
35
|
+
refs: refs.to_s.split(",").map(&:strip).reject(&:empty?))
|
|
36
|
+
puts JSON.generate({"case" => case_id, "suggestion" => state.to_s})
|
|
37
|
+
rescue Ace::Hitl::Organisms::HitlManager::UnknownCaseError,
|
|
38
|
+
Ace::Hitl::Organisms::HitlManager::AmbiguousReferenceError,
|
|
39
|
+
Ace::Hitl::Organisms::HitlManager::SuggestionRefusedError,
|
|
40
|
+
Ace::Hitl::Organisms::HitlManager::TerminalCaseError => e
|
|
41
|
+
raise Ace::Support::Cli::Error.new("suggestion refused: #{e.message}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Hitl
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# `ace-hitl sweep [--apply]` (task 8xs.t.pu0.6): qualify transport
|
|
10
|
+
# leftovers against the CANONICAL case files, list-first. The
|
|
11
|
+
# only deletion candidates are ask-wake request files whose case
|
|
12
|
+
# is terminally closed or cancelled; active cases (pending /
|
|
13
|
+
# received / applied), unknown ids, unparseable requests and the
|
|
14
|
+
# canonical decision history are always preserved. Without
|
|
15
|
+
# --apply nothing is deleted.
|
|
16
|
+
class Sweep < Ace::Support::Cli::Command
|
|
17
|
+
include Ace::Support::Cli::Base
|
|
18
|
+
|
|
19
|
+
desc "List (and with --apply, remove) transport leftovers of terminally closed cases"
|
|
20
|
+
|
|
21
|
+
option :apply, type: :boolean, default: false,
|
|
22
|
+
desc: "Remove the qualified leftovers (default: list only)"
|
|
23
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
24
|
+
|
|
25
|
+
def call(apply: false, **)
|
|
26
|
+
report = Ace::Hitl::Organisms::HitlManager.new.sweep(apply: apply)
|
|
27
|
+
puts JSON.pretty_generate(report)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/ace/hitl/cli.rb
CHANGED
|
@@ -4,6 +4,13 @@ require "ace/support/cli"
|
|
|
4
4
|
require_relative "../hitl/version"
|
|
5
5
|
require_relative "cli/commands/create"
|
|
6
6
|
require_relative "cli/commands/ask"
|
|
7
|
+
require_relative "cli/commands/reply"
|
|
8
|
+
require_relative "cli/commands/apply"
|
|
9
|
+
require_relative "cli/commands/close"
|
|
10
|
+
require_relative "cli/commands/sweep"
|
|
11
|
+
require_relative "cli/commands/suggest"
|
|
12
|
+
require_relative "cli/commands/lesson"
|
|
13
|
+
require_relative "cli/commands/history"
|
|
7
14
|
require_relative "cli/commands/send"
|
|
8
15
|
require_relative "cli/commands/operator_question"
|
|
9
16
|
require_relative "cli/commands/read"
|
|
@@ -43,7 +50,14 @@ module Ace
|
|
|
43
50
|
["cancel", "Cancel one HITL relay request with an audited reason"],
|
|
44
51
|
["pending", "List answerable HITL relay requests (transport)"],
|
|
45
52
|
["states", "List public HITL lifecycle projections (transport)"],
|
|
46
|
-
["duty", "Project pending and escalated HITL requests (transport)"]
|
|
53
|
+
["duty", "Project pending and escalated HITL requests (transport)"],
|
|
54
|
+
["reply", "Receive a delivered human answer for one exact case (stdin)"],
|
|
55
|
+
["suggest", "Write the Second Captain's marked advisory suggestion into one exact case"],
|
|
56
|
+
["lesson", "Write the lesson drawn from the human decision vs the suggestion"],
|
|
57
|
+
["history", "List the citable decision history (answered, non-secret cases)"],
|
|
58
|
+
["apply", "Record the answer's applied effect with its owner reference"],
|
|
59
|
+
["close", "Close a case terminally after its applied effect"],
|
|
60
|
+
["sweep", "List (and with --apply, remove) transport leftovers of terminal cases"]
|
|
47
61
|
].freeze
|
|
48
62
|
|
|
49
63
|
HELP_EXAMPLES = [
|
|
@@ -67,6 +81,13 @@ module Ace
|
|
|
67
81
|
register "observe", CLI::Commands::Observe
|
|
68
82
|
register "update", CLI::Commands::Update
|
|
69
83
|
register "ask", CLI::Commands::Ask
|
|
84
|
+
register "reply", CLI::Commands::Reply
|
|
85
|
+
register "apply", CLI::Commands::Apply
|
|
86
|
+
register "close", CLI::Commands::Close
|
|
87
|
+
register "sweep", CLI::Commands::Sweep
|
|
88
|
+
register "suggest", CLI::Commands::Suggest
|
|
89
|
+
register "lesson", CLI::Commands::Lesson
|
|
90
|
+
register "history", CLI::Commands::History
|
|
70
91
|
register "send", CLI::Commands::Send
|
|
71
92
|
register "wait", CLI::Commands::Wait
|
|
72
93
|
register "deliver", CLI::Commands::Deliver
|