ace-coworker 0.6.2 → 0.7.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/.ace-defaults/coworker/roles/second-captain.yml +18 -0
- data/CHANGELOG.md +24 -1
- data/README.md +2 -1
- data/docs/usage.md +33 -19
- data/handbook/workflow-instructions/coworker/integrator.wf.md +6 -2
- data/handbook/workflow-instructions/coworker/second-captain.wf.md +88 -0
- data/lib/ace/coworker/atoms/config_schema.rb +1 -0
- data/lib/ace/coworker/atoms/integration_gate.rb +59 -12
- data/lib/ace/coworker/atoms/prompt_template.rb +3 -1
- data/lib/ace/coworker/cli/commands/commission.rb +2 -1
- data/lib/ace/coworker/cli/commands/gate.rb +21 -5
- data/lib/ace/coworker/organisms/commission_orchestrator.rb +16 -6
- data/lib/ace/coworker/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b1ca4d493124efc790ec4f9e9ebe719b341906f45d0f8913c1005bb2ff86a94
|
|
4
|
+
data.tar.gz: adeebb47b7350576f6aabb79974efd1e8683869b8623d5f6b84fbb8d5f86abb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8e78949c6ca07838eb12e7580704aedf918f59436a1fcd5354da812308c442e8b4eff4a42fb75334af231d04206ad511e4ce7d48a3fcc9299155f098d61de98
|
|
7
|
+
data.tar.gz: 60d8735e3fbb344584e9ded3d27dfac0cd32d0f34d5a46f305342dcab3b8715c361bd88e2ef5888672a1a786a3685eb0d19c0a70172c0ec6a6252571c3c65194
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Second Captain role preset (shipped gem defaults, overridable per project
|
|
3
|
+
# via .ace/coworker/roles/second-captain.yml). Advisory ONLY: delegated per
|
|
4
|
+
# HITL case to propose a marked answer with rationale and grounds (task
|
|
5
|
+
# 8xs.t.pu0.b). Never answers in the human's name, never approves or gates
|
|
6
|
+
# work, never touches secret/OTP cases.
|
|
7
|
+
name: second-captain
|
|
8
|
+
description: Advisory Second Captain; proposes one marked answer with rationale and grounds into the exact HITL case and stops — never a human decision, approval, or gate
|
|
9
|
+
instructions: wfi://coworker/second-captain
|
|
10
|
+
start_prompt: >-
|
|
11
|
+
Work as the {{role}} on case {{case}} (task {{task}}): read the exact HITL
|
|
12
|
+
case and its referenced evidence, write one marked suggestion with
|
|
13
|
+
rationale and grounds through assignment {{assignment}}, reporting to the
|
|
14
|
+
overseer at {{overseer}} — then stop.
|
|
15
|
+
reporting:
|
|
16
|
+
channel: hitl
|
|
17
|
+
errors:
|
|
18
|
+
on_failure: report
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,30 @@ All notable changes to this project 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.7.0] - 2026-10-30
|
|
9
|
+
|
|
10
|
+
### Fixed (review rounds 3/3b)
|
|
11
|
+
|
|
12
|
+
- The integration gate rejects anonymous formal approvals (empty approver);
|
|
13
|
+
`commission --hitl-case CASE_ID` renders `{{case}}` for per-case presets.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `second-captain` advisory role preset + `wfi://coworker/second-captain`
|
|
18
|
+
workflow (task 8xs.t.pu0.b): delegated per HITL case to write one marked
|
|
19
|
+
suggestion and stop; never answers in the human's name, approves, gates
|
|
20
|
+
work, or touches secret cases.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Integration gate binds ALL evidence to the exact candidate head (task
|
|
25
|
+
8xs.t.pu0.8): test evidence must carry the head it covered (bare
|
|
26
|
+
executed/passed blocks; a different tested head is stale evidence), and the
|
|
27
|
+
formal approval — `--approval-head`/`--approval-by` — must name the same
|
|
28
|
+
head and come from an account other than the author. The integrator
|
|
29
|
+
re-reads the HEAD immediately before merging through the merge adapter's
|
|
30
|
+
expected-head condition, so the gate-to-merge race refuses a stale
|
|
31
|
+
candidate. CI stays advisory.
|
|
9
32
|
|
|
10
33
|
## [0.6.2] - 2026-10-23
|
|
11
34
|
|
data/README.md
CHANGED
|
@@ -23,8 +23,9 @@ ace-coworker resume [--dry-run] # Resume interrupted work in this
|
|
|
23
23
|
ace-coworker prepare --task REF --roles worker,reviewer [--to ADDR] [--dry-run]
|
|
24
24
|
ace-coworker roles # List role presets (gem + project)
|
|
25
25
|
ace-coworker commission --role worker --task REF --assignment ID --overseer ADDR
|
|
26
|
+
[--hitl-case CASE_ID] # renders {{case}} in per-case presets
|
|
26
27
|
ace-coworker gate --candidate-head SHA --review-head SHA --review-verdict accept
|
|
27
|
-
--tests executed,passed --dep LEAF=integrated
|
|
28
|
+
--tests executed,passed --tests-head SHA --dep LEAF=integrated
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
- `prepare` — the planner side: accept a preparation commission (task +
|
data/docs/usage.md
CHANGED
|
@@ -171,28 +171,42 @@ errors, non-zero exit).
|
|
|
171
171
|
```
|
|
172
172
|
ace-coworker gate --review-verdict accept|continue|escalate
|
|
173
173
|
[--candidate-head SHA] [--review-head SHA]
|
|
174
|
-
[--tests executed,passed] [--
|
|
174
|
+
[--tests executed,passed] [--tests-head SHA]
|
|
175
|
+
[--approval-head SHA --approval-by ACCOUNT] [--author ACCOUNT]
|
|
176
|
+
[--dep LEAF=integrated|missing ...]
|
|
175
177
|
[--format table|json] [-q] [-d]
|
|
176
178
|
```
|
|
177
179
|
|
|
178
|
-
The integrator role's pre-merge gate (8xg.t.sbz.4.3
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
`--dep`
|
|
194
|
-
|
|
195
|
-
|
|
180
|
+
The integrator role's pre-merge gate (8xg.t.sbz.4.3; HEAD binding of
|
|
181
|
+
evidence per 8xs.t.pu0.8): a pure, stateless evaluation of the
|
|
182
|
+
project-defined checks. `--candidate-head` is the current candidate head
|
|
183
|
+
(omit it when genuinely unknown - the gate then blocks on the missing
|
|
184
|
+
evidence instead of guessing); `--review-head` is the candidate head the
|
|
185
|
+
independent review verdict covers; `--tests-head` is the head the
|
|
186
|
+
executed test run covered — test evidence without a head, or bound to a
|
|
187
|
+
different head, BLOCKS the merge (a code change or rebase after the tests
|
|
188
|
+
invalidates that basis); `--approval-head`/`--approval-by` supply the
|
|
189
|
+
formal approval's head and account for lanes that require one (an
|
|
190
|
+
approval bound to another head blocks, and an approval by the AUTHOR's
|
|
191
|
+
own account blocks — the formal approver is a different configured lane
|
|
192
|
+
account); each `--dep NAME=integrated|missing` pair states one
|
|
193
|
+
dependency leaf (`missing` or any other value = not integrated; the pair
|
|
194
|
+
must contain `=`, and repeating a name is an error). Empty or omitted
|
|
195
|
+
`--dep` means the caller enumerated no dependencies - the gate trusts the
|
|
196
|
+
caller's dependency enumeration. Prints `ready` or `blocked` with
|
|
197
|
+
per-check reasons (dependencies, candidate drift, review
|
|
198
|
+
freshness/verdict, executed+passing+HEAD-bound tests, formal approval).
|
|
199
|
+
`blocked` is a legitimate gate outcome (exit 0) - the role withholds the
|
|
200
|
+
merge and escalates; only malformed input (an invalid `--dep` pair, a
|
|
201
|
+
duplicate `--dep` name, or an invalid `--tests` token) fails with a
|
|
202
|
+
non-zero exit. Heads are recorded and compared as full lowercase SHAs.
|
|
203
|
+
Nothing is persisted and no git operation is performed: the gate decides
|
|
204
|
+
readiness only — the integrator reads the current HEAD immediately
|
|
205
|
+
before merging and passes it as the merge adapter's expected-head
|
|
206
|
+
condition, so a candidate that moves between gate and merge is refused
|
|
207
|
+
by the adapter (the race never merges a stale candidate). CI status is
|
|
208
|
+
advisory (Captain's decision): a red CI does not block a merge backed by
|
|
209
|
+
executed passing tests and an accepting independent review.
|
|
196
210
|
|
|
197
211
|
### `ace-coworker commission`
|
|
198
212
|
|
|
@@ -82,12 +82,16 @@ the exact candidate head. Otherwise WITHHOLD the merge and report/escalate.
|
|
|
82
82
|
3. **Evaluate the gate (deterministic).**
|
|
83
83
|
- `ace-coworker gate --candidate-head <sha> --review-head <sha>
|
|
84
84
|
--review-verdict accept|continue|escalate --tests executed,passed
|
|
85
|
-
--dep <leaf>=integrated --dep <leaf>=missing`
|
|
85
|
+
--tests-head <sha> --dep <leaf>=integrated --dep <leaf>=missing`
|
|
86
86
|
(repeat `--dep` per dependency; omit `--candidate-head`/`--review-head`
|
|
87
87
|
when genuinely unknown so the gate blocks on the missing evidence).
|
|
88
|
+
`--tests-head` is REQUIRED whenever test evidence is supplied: it is
|
|
89
|
+
the candidate head the executed test run covered, and the gate
|
|
90
|
+
blocks on stale test evidence without it.
|
|
88
91
|
- The gate (`Ace::Coworker::Atoms::IntegrationGate`) is pure: it returns
|
|
89
92
|
`ready` or `blocked` with per-check reasons (dependencies, candidate
|
|
90
|
-
drift, review freshness/verdict, executed+passing tests
|
|
93
|
+
drift, review freshness/verdict, executed+passing tests bound to the
|
|
94
|
+
tested head). `blocked` is a
|
|
91
95
|
legitimate outcome with exit 0; only invalid input fails the command.
|
|
92
96
|
- A candidate change after the review (candidate head != reviewed head)
|
|
93
97
|
invalidates the review: the gate reports a stale review even when the
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coworker-second-captain
|
|
3
|
+
description: Second Captain advisory coworker role — propose an answer with rationale and grounds into the exact HITL case; never answer in the human's name, never approve, never gate work
|
|
4
|
+
allowed-tools: Bash, Read
|
|
5
|
+
doc-type: workflow
|
|
6
|
+
title: Second Captain Workflow
|
|
7
|
+
purpose: Bind the advisory role's behavior: one marked suggestion per case/revision with rationale and grounds in the same HITL thread, non-blocking on delivery, discarded when the human answered first, excluded from secret cases.
|
|
8
|
+
ace-docs:
|
|
9
|
+
last-updated: '2026-10-30'
|
|
10
|
+
last-checked: '2026-10-30'
|
|
11
|
+
---
|
|
12
|
+
# Second Captain Workflow
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
|
|
16
|
+
You are the Second Captain: an advisory coworker delegated for ONE HITL
|
|
17
|
+
case (task 8xs.t.pu0.b). You draft a proposed answer so the Captain sees a
|
|
18
|
+
marked suggestion with its reasoning in the same thread. You are an agent,
|
|
19
|
+
not a second human: you never answer in the Captain's name, never approve
|
|
20
|
+
or release gated work, and your suggestion is never treated as human
|
|
21
|
+
consent.
|
|
22
|
+
|
|
23
|
+
## Instructions
|
|
24
|
+
|
|
25
|
+
Run the loaded workflow as the source of truth and execute it end-to-end
|
|
26
|
+
instead of only summarizing it.
|
|
27
|
+
|
|
28
|
+
### 1) Read the exact case
|
|
29
|
+
|
|
30
|
+
Open the delegated case file. Work ONLY from the case content and the
|
|
31
|
+
grounds it references (task/assignment/report evidence). If the grounds are
|
|
32
|
+
insufficient for a defensible proposal, say so in the suggestion — never
|
|
33
|
+
fabricate precedent or confidence.
|
|
34
|
+
|
|
35
|
+
### 2) Learn from every real decision
|
|
36
|
+
|
|
37
|
+
When the Captain answers a case that carried your suggestion, compare the
|
|
38
|
+
decision with your proposal and write ONE lesson at that case (task
|
|
39
|
+
8xs.t.pu0.c):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ace-hitl lesson CASE_ID --by second-captain \
|
|
43
|
+
--relation accepted|rejected|modified \
|
|
44
|
+
--takeaway "what the next suggestion should learn"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A case without your suggestion has NOTHING to compare — never fabricate
|
|
48
|
+
agreement. Identical reprocessing is a no-op. The lesson cites the exact
|
|
49
|
+
question/suggestion/decision revisions; it never overwrites them and never
|
|
50
|
+
changes the decision's author.
|
|
51
|
+
|
|
52
|
+
### 3) Ground the next suggestion in citable history
|
|
53
|
+
|
|
54
|
+
Before proposing, read the decision history available to THIS project
|
|
55
|
+
(`ace-hitl history`): answered, non-secret cases with their suggestions,
|
|
56
|
+
decisions and lessons. Cite the real case ids you relied on and note the
|
|
57
|
+
limits of the similarity; a NEWER explicit Captain correction outranks an
|
|
58
|
+
older suggestion. When you have no applicable precedent, SAY SO — a visible
|
|
59
|
+
lack of grounds is honest, a fabricated precedent is not. Secret/OTP values
|
|
60
|
+
never enter your proposals, takeaways, or citations.
|
|
61
|
+
|
|
62
|
+
### 4) Write the suggestion into the case
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
ace-hitl suggest CASE_ID --by second-captain \
|
|
66
|
+
--text "proposed answer" \
|
|
67
|
+
--rationale "why this is the defensible choice — cite case ids you relied on" \
|
|
68
|
+
--refs task:8xs.t.pu0,assignment:job-7
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
One suggestion per case/revision; an identical repeat is a no-op. The
|
|
72
|
+
suggestion lands as its own marked section in the case — never as the
|
|
73
|
+
Answer.
|
|
74
|
+
|
|
75
|
+
### 5) Stop
|
|
76
|
+
|
|
77
|
+
Your assignment ends with the written suggestion. Do not wake the Captain,
|
|
78
|
+
do not open a new case, do not apply or await the answer.
|
|
79
|
+
|
|
80
|
+
## Boundaries
|
|
81
|
+
|
|
82
|
+
- A question marked `secret: true` (e.g. OTP) is NEVER delegated to you; if
|
|
83
|
+
you receive one, report the mismatch and stop.
|
|
84
|
+
- If the Captain already answered, DISCARD your late proposal, mark your
|
|
85
|
+
assignment complete, and do not notify anyone — the human decision stands
|
|
86
|
+
and needs no second approval.
|
|
87
|
+
- Your advisory rights cover reading the case and its referenced evidence
|
|
88
|
+
only — no merges, no releases, no token use.
|
|
@@ -52,6 +52,7 @@ module Ace
|
|
|
52
52
|
"task" => str(raw["task"]),
|
|
53
53
|
"assignment" => str(raw["assignment"]),
|
|
54
54
|
"overseer" => str(raw["overseer"]),
|
|
55
|
+
"case" => str(raw["case"]),
|
|
55
56
|
"reporting" => {"channel" => nested_str(raw, "reporting", "channel").empty? ? "hitl" : nested_str(raw, "reporting", "channel")},
|
|
56
57
|
"errors" => {"on_failure" => nested_str(raw, "errors", "on_failure").empty? ? "report" : nested_str(raw, "errors", "on_failure")}
|
|
57
58
|
}.compact
|
|
@@ -3,26 +3,39 @@
|
|
|
3
3
|
module Ace
|
|
4
4
|
module Coworker
|
|
5
5
|
module Atoms
|
|
6
|
-
# Pure pre-merge integration gate (8xg.t.sbz.4.3 / contract
|
|
7
|
-
# integrator merges only a candidate
|
|
8
|
-
# integrated predecessors, (b) current
|
|
9
|
-
# head (no drift), (c) tests were
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
6
|
+
# Pure pre-merge integration gate (8xg.t.sbz.4.3 / contract, HEAD
|
|
7
|
+
# binding per 8xs.t.pu0.8): the integrator merges only a candidate
|
|
8
|
+
# whose (a) dependencies are integrated predecessors, (b) current
|
|
9
|
+
# head matches the reviewed head (no drift), (c) tests were
|
|
10
|
+
# executed, passed, AND BOUND TO THE EXACT CANDIDATE HEAD (bare
|
|
11
|
+
# executed/passed without a head covers nothing), (d) a fresh
|
|
12
|
+
# independent review verdict accepts the EXACT candidate head, and
|
|
13
|
+
# (e) the formal approval — when required — also names this head
|
|
14
|
+
# and was given by a NAMED, DIFFERENT account than the author (an
|
|
15
|
+
# anonymous approval proves nothing). Any
|
|
16
|
+
# unknown or contradictory fact is a blocking reason - a code
|
|
17
|
+
# change or rebase after tests or review invalidates those bases
|
|
18
|
+
# for the old candidate; uncertainty must be verified against the
|
|
19
|
+
# real worktree/reports, never guessed or retried blind. Explicit
|
|
20
|
+
# :blocked outcome with reasons, never an exception. No IO, no
|
|
21
|
+
# writes, no merges: the gate decides readiness only; the role
|
|
22
|
+
# drives git/ace-git (the merge adapter's expected-head condition
|
|
23
|
+
# refuses a candidate that moved between gate and merge).
|
|
16
24
|
module IntegrationGate
|
|
17
25
|
ACCEPT_VERDICTS = %w[accept].freeze
|
|
18
26
|
|
|
19
27
|
# @param input [Hash] string-keyed facts:
|
|
20
28
|
# "candidate_head" => String (current head of the candidate)
|
|
21
29
|
# "review" => {"head" => String reviewed head, "verdict" => String}
|
|
22
|
-
# "tests" => {"executed" => bool, "passed" => bool
|
|
30
|
+
# "tests" => {"executed" => bool, "passed" => bool,
|
|
31
|
+
# "head" => String head the test run covered}
|
|
32
|
+
# "approval" => {"head" => String, "by" => String account} (a
|
|
33
|
+
# nil/empty approval skips the formal-approval check; lanes
|
|
34
|
+
# that require it must supply it)
|
|
35
|
+
# "author" => String candidate author account (optional)
|
|
23
36
|
# "dependencies" => {name => bool (integrated predecessor)}
|
|
24
37
|
# @return [Hash] {verdict: :ready | :blocked, reasons: [String],
|
|
25
|
-
# failed_checks: [:dependencies|:candidate|:review|:tests|:input]}
|
|
38
|
+
# failed_checks: [:dependencies|:candidate|:review|:tests|:approval|:input]}
|
|
26
39
|
def self.evaluate(input)
|
|
27
40
|
unless input.is_a?(Hash) && input.keys.all? { |k| k.is_a?(String) }
|
|
28
41
|
return {verdict: :blocked,
|
|
@@ -70,6 +83,40 @@ module Ace
|
|
|
70
83
|
reasons << "tests not passing: executed tests failed or their outcome is unknown"
|
|
71
84
|
failed << :tests
|
|
72
85
|
end
|
|
86
|
+
tests_head = str(tests["head"])
|
|
87
|
+
if tests_head.empty?
|
|
88
|
+
reasons << "test evidence not bound to a candidate head: executed/passed covers no " \
|
|
89
|
+
"verifiable candidate - re-run the tests on the current head and record it"
|
|
90
|
+
failed << :tests
|
|
91
|
+
elsif !candidate_head.empty? && tests_head.downcase != candidate_head.downcase
|
|
92
|
+
reasons << "stale test evidence: candidate #{candidate_head} is not the tested head #{tests_head}"
|
|
93
|
+
failed << :tests
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
approval = hash(input["approval"])
|
|
97
|
+
unless approval.empty?
|
|
98
|
+
approval_head = str(approval["head"])
|
|
99
|
+
approved_by = str(approval["by"])
|
|
100
|
+
if approval_head.empty?
|
|
101
|
+
reasons << "formal approval not bound to a candidate head: the approval covers no " \
|
|
102
|
+
"verifiable candidate"
|
|
103
|
+
failed << :approval
|
|
104
|
+
elsif !candidate_head.empty? && approval_head.downcase != candidate_head.downcase
|
|
105
|
+
reasons << "stale formal approval: candidate #{candidate_head} is not the approved " \
|
|
106
|
+
"head #{approval_head}"
|
|
107
|
+
failed << :approval
|
|
108
|
+
end
|
|
109
|
+
author = str(input["author"])
|
|
110
|
+
if approved_by.empty?
|
|
111
|
+
reasons << "formal approval is anonymous: no approving account is recorded — " \
|
|
112
|
+
"the author≠approver guarantee is vacuous; record the approving lane account"
|
|
113
|
+
failed << :approval
|
|
114
|
+
elsif !author.empty? && approved_by.downcase == author.downcase
|
|
115
|
+
reasons << "formal approval by the author's own account '#{approved_by}': the formal " \
|
|
116
|
+
"approver must be a different configured lane account"
|
|
117
|
+
failed << :approval
|
|
118
|
+
end
|
|
119
|
+
end
|
|
73
120
|
|
|
74
121
|
{verdict: reasons.empty? ? :ready : :blocked,
|
|
75
122
|
reasons: reasons, failed_checks: failed.uniq}
|
|
@@ -7,7 +7,9 @@ module Ace
|
|
|
7
7
|
# placeholders from a context hash; unknown placeholders stay literal.
|
|
8
8
|
# No IO.
|
|
9
9
|
class PromptTemplate
|
|
10
|
-
|
|
10
|
+
# `case` carries the HITL case id for per-case delegation roles
|
|
11
|
+
# (e.g. second-captain); roles without it simply never reference it.
|
|
12
|
+
PLACEHOLDERS = %w[task assignment role overseer case].freeze
|
|
11
13
|
PLACEHOLDER_PATTERN = /\{\{(#{PLACEHOLDERS.map { |p| Regexp.escape(p) }.join('|')})\}\}/.freeze
|
|
12
14
|
|
|
13
15
|
# @param template [String] preset start_prompt with {{placeholders}}
|
|
@@ -15,6 +15,7 @@ module Ace
|
|
|
15
15
|
option :task, desc: "Task reference that must resolve in this worktree (required)"
|
|
16
16
|
option :assignment, desc: "Assignment id pinned to the role (required)"
|
|
17
17
|
option :overseer, desc: "Overseer reporting address (required)"
|
|
18
|
+
option :"hitl-case", desc: "HITL case id for per-case delegation roles (renders the preset's {{case}})"
|
|
18
19
|
option :"start-prompt", desc: "Literal start prompt overriding the preset template"
|
|
19
20
|
option :config, desc: "Config path (default from cascade: .ace-local/coworker/config.yml)"
|
|
20
21
|
option :force, type: :boolean, default: false, desc: "Allow overwriting an existing config"
|
|
@@ -47,7 +48,7 @@ module Ace
|
|
|
47
48
|
orchestrator = @orchestrator || build_orchestrator(options[:config])
|
|
48
49
|
result = orchestrator.call(
|
|
49
50
|
role: role, task: task, assignment: assignment, overseer: overseer,
|
|
50
|
-
start_prompt: options[:"start-prompt"],
|
|
51
|
+
start_prompt: options[:"start-prompt"], hitl_case: options[:"hitl-case"],
|
|
51
52
|
force: options[:force] == true, dry_run: options[:"dry-run"] == true
|
|
52
53
|
)
|
|
53
54
|
|
|
@@ -13,26 +13,34 @@ module Ace
|
|
|
13
13
|
|
|
14
14
|
example [
|
|
15
15
|
"gate --candidate-head 1a2b3c4 --review-head 1a2b3c4 --review-verdict accept \\",
|
|
16
|
-
" --tests executed,passed --dep 8xg.t.sbz.4.2=integrated",
|
|
16
|
+
" --tests executed,passed --tests-head 1a2b3c4 --dep 8xg.t.sbz.4.2=integrated \\",
|
|
17
|
+
" --approval-head 1a2b3c4 --approval-by lab-merge-ace --author lab-overseer",
|
|
17
18
|
"gate --candidate-head 1a2b3c4 --review-head 0d0e0f --review-verdict accept \\",
|
|
18
|
-
" --tests executed,passed --dep 8xg.t.sbz.4.2=missing"
|
|
19
|
+
" --tests executed,passed --tests-head 1a2b3c4 --dep 8xg.t.sbz.4.2=missing"
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
option :candidate_head, desc: "Current head of the candidate branch (omit when unknown)"
|
|
22
23
|
option :review_head, desc: "Candidate head the review verdict covers"
|
|
23
24
|
option :review_verdict, required: true, desc: "Independent review verdict (accept, continue, escalate)"
|
|
24
25
|
option :tests, type: :array, default: [], desc: "Test evidence flags: executed, passed"
|
|
26
|
+
option :tests_head, desc: "Candidate head the executed test run covered (required with test evidence)"
|
|
27
|
+
option :approval_head, desc: "Candidate head the formal approval covers (lanes requiring formal approval)"
|
|
28
|
+
option :approval_by, desc: "Account that gave the formal approval"
|
|
29
|
+
option :author, desc: "Candidate author account (the formal approver must differ)"
|
|
25
30
|
option :dep, type: :array, default: [], desc: "Dependency states as name=integrated|missing pairs"
|
|
26
31
|
option :format, default: "table", desc: "Output format (table, json)"
|
|
27
32
|
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
28
33
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
29
34
|
|
|
30
|
-
def call(candidate_head: nil, review_head: nil, review_verdict: nil, tests: [],
|
|
31
|
-
|
|
35
|
+
def call(candidate_head: nil, review_head: nil, review_verdict: nil, tests: [],
|
|
36
|
+
tests_head: nil, approval_head: nil, approval_by: nil, author: nil, dep: [],
|
|
37
|
+
format: "table", **options)
|
|
32
38
|
result = Atoms::IntegrationGate.evaluate(
|
|
33
39
|
"candidate_head" => candidate_head.to_s,
|
|
34
40
|
"review" => {"head" => review_head.to_s, "verdict" => review_verdict.to_s},
|
|
35
|
-
"tests" => tests_flags(tests),
|
|
41
|
+
"tests" => tests_flags(tests).merge("head" => tests_head.to_s),
|
|
42
|
+
"approval" => approval_for(approval_head, approval_by),
|
|
43
|
+
"author" => author.to_s,
|
|
36
44
|
"dependencies" => dependency_states(dep)
|
|
37
45
|
)
|
|
38
46
|
|
|
@@ -47,6 +55,14 @@ module Ace
|
|
|
47
55
|
|
|
48
56
|
private
|
|
49
57
|
|
|
58
|
+
# A formal approval participates only when the lane supplied
|
|
59
|
+
# approval evidence; omitted approval facts skip the check.
|
|
60
|
+
def approval_for(approval_head, approval_by)
|
|
61
|
+
return {} if approval_head.to_s.strip.empty? && approval_by.to_s.strip.empty?
|
|
62
|
+
|
|
63
|
+
{"head" => approval_head.to_s, "by" => approval_by.to_s}
|
|
64
|
+
end
|
|
65
|
+
|
|
50
66
|
def tests_flags(tests)
|
|
51
67
|
flags = Array(tests).flat_map { |value| value.to_s.split(",") }.map { |f| f.strip.downcase }.reject(&:empty?)
|
|
52
68
|
flags.each do |flag|
|
|
@@ -56,7 +56,8 @@ module Ace
|
|
|
56
56
|
# @raise [Error] on unknown role, identity mismatch, invalid
|
|
57
57
|
# assembly, a refused overwrite, a config path escaping the
|
|
58
58
|
# project root, or a directory config target
|
|
59
|
-
def call(role:, task:, assignment:, overseer:, start_prompt: nil,
|
|
59
|
+
def call(role:, task:, assignment:, overseer:, start_prompt: nil, hitl_case: nil,
|
|
60
|
+
force: false, dry_run: false)
|
|
60
61
|
preset = @preset_loader.fetch(role)
|
|
61
62
|
|
|
62
63
|
# Canonicalize the task identity first: the assignment match and
|
|
@@ -69,10 +70,17 @@ module Ace
|
|
|
69
70
|
canonical_task = resolved.id.to_s
|
|
70
71
|
@assignment_metadata.verify_task_match!(assignment, canonical_task)
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
hitl_case = hitl_case.to_s.strip
|
|
74
|
+
if start_prompt.to_s.strip.empty? && preset["start_prompt"].to_s.include?("{{case}}") && hitl_case.empty?
|
|
75
|
+
raise Error,
|
|
76
|
+
"the '#{role}' start_prompt delegates to a HITL case ({{case}}); " \
|
|
77
|
+
"pass the exact case id (--hitl-case CASE_ID) or a literal --start-prompt"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
prompt = start_prompt.to_s.strip.empty? ? render_prompt(preset, role, canonical_task, assignment, overseer, hitl_case) : start_prompt
|
|
73
81
|
|
|
74
82
|
config = build_config(preset, role: role, task: canonical_task, assignment: assignment,
|
|
75
|
-
overseer: overseer, prompt: prompt)
|
|
83
|
+
overseer: overseer, prompt: prompt, hitl_case: hitl_case)
|
|
76
84
|
errors = Atoms::ConfigSchema.errors(config)
|
|
77
85
|
unless errors.empty?
|
|
78
86
|
raise Error, "commissioned config is invalid: #{errors.join('; ')}"
|
|
@@ -91,12 +99,13 @@ module Ace
|
|
|
91
99
|
|
|
92
100
|
private
|
|
93
101
|
|
|
94
|
-
def render_prompt(preset, role, task, assignment, overseer)
|
|
102
|
+
def render_prompt(preset, role, task, assignment, overseer, hitl_case)
|
|
95
103
|
Atoms::PromptTemplate.render(preset["start_prompt"],
|
|
96
|
-
{"task" => task, "assignment" => assignment, "role" => role,
|
|
104
|
+
{"task" => task, "assignment" => assignment, "role" => role,
|
|
105
|
+
"overseer" => overseer, "case" => hitl_case})
|
|
97
106
|
end
|
|
98
107
|
|
|
99
|
-
def build_config(preset, role:, task:, assignment:, overseer:, prompt:)
|
|
108
|
+
def build_config(preset, role:, task:, assignment:, overseer:, prompt:, hitl_case: "")
|
|
100
109
|
config = {
|
|
101
110
|
"role" => role,
|
|
102
111
|
"instructions" => preset["instructions"],
|
|
@@ -105,6 +114,7 @@ module Ace
|
|
|
105
114
|
"assignment" => assignment,
|
|
106
115
|
"overseer" => overseer
|
|
107
116
|
}
|
|
117
|
+
config["case"] = hitl_case unless hitl_case.empty?
|
|
108
118
|
config["toolchain"] = "installed" if Ace::Support::Cli::Toolchain.installed?
|
|
109
119
|
config["reporting"] = {"channel" => preset["reporting"]} unless preset["reporting"].to_s.empty?
|
|
110
120
|
config["errors"] = {"on_failure" => preset["errors"]} unless preset["errors"].to_s.empty?
|
data/lib/ace/coworker/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-coworker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
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-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -236,6 +236,7 @@ files:
|
|
|
236
236
|
- ".ace-defaults/coworker/config.yml"
|
|
237
237
|
- ".ace-defaults/coworker/roles/integrator.yml"
|
|
238
238
|
- ".ace-defaults/coworker/roles/reviewer.yml"
|
|
239
|
+
- ".ace-defaults/coworker/roles/second-captain.yml"
|
|
239
240
|
- ".ace-defaults/coworker/roles/spec.yml"
|
|
240
241
|
- ".ace-defaults/coworker/roles/worker.yml"
|
|
241
242
|
- ".ace-defaults/coworker/sequences/integrator.yml"
|
|
@@ -250,6 +251,7 @@ files:
|
|
|
250
251
|
- exe/ace-coworker
|
|
251
252
|
- handbook/workflow-instructions/coworker/integrator.wf.md
|
|
252
253
|
- handbook/workflow-instructions/coworker/reviewer.wf.md
|
|
254
|
+
- handbook/workflow-instructions/coworker/second-captain.wf.md
|
|
253
255
|
- handbook/workflow-instructions/coworker/spec.wf.md
|
|
254
256
|
- handbook/workflow-instructions/coworker/worker.wf.md
|
|
255
257
|
- lib/ace/coworker.rb
|