ace-coworker 0.6.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6901776c6bcef11b2bfb48ddaeb2377dd830f352d01c79bddc59254a4f799849
4
- data.tar.gz: 604583cbbb6eab225faccd01025cc5efdce6cad789bc7faca87091713cdf7a7b
3
+ metadata.gz: 7b1ca4d493124efc790ec4f9e9ebe719b341906f45d0f8913c1005bb2ff86a94
4
+ data.tar.gz: adeebb47b7350576f6aabb79974efd1e8683869b8623d5f6b84fbb8d5f86abb9
5
5
  SHA512:
6
- metadata.gz: be41301547584600fba77e414d603bed84caa025bdae72601644bbbd6b62569167ed72cb20da61ecb45c8c6deeefcb5367beb11f5914f1e3471f2180d847bea6
7
- data.tar.gz: 8e29e8818c437ae80ffd3de61976dafe166db7c215d47003176e3e5ae43ea995d4db969aa8e7168be44c8207db3a93a41e1052760d9b6be6f6a49eed184dbfa4
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,37 @@ 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
- ## [Unreleased]
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.
32
+
33
+ ## [0.6.2] - 2026-10-23
34
+
35
+ ### Added
36
+
37
+ - Formal reviewer-owned, exact-head provider approval and read-only approval verification through the forge-neutral review flow; comments do not substitute for approval.
38
+
9
39
 
10
40
  ## [0.6.1] - 2026-10-20
11
41
 
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] [--dep LEAF=integrated|missing ...]
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): a pure, stateless
179
- evaluation of the four project-defined checks. `--candidate-head` is the
180
- current candidate head (omit it when genuinely unknown - the gate then
181
- blocks on the missing evidence instead of guessing); `--review-head` is the
182
- candidate head the independent review verdict covers; `--tests` takes the
183
- `executed,passed` evidence flags (any other token is an error, not
184
- silent missing evidence); each `--dep NAME=integrated|missing` pair
185
- states one dependency leaf (`missing` or any other value = not
186
- integrated; the pair must contain `=`, and repeating a name is an
187
- error). Empty or omitted `--dep` means the caller enumerated no
188
- dependencies - the gate trusts the caller's dependency enumeration.
189
- Prints `ready` or `blocked` with per-check reasons (dependencies, candidate
190
- drift, review freshness/verdict, executed+passing tests). `blocked` is a
191
- legitimate gate outcome (exit 0) - the role withholds the merge and
192
- escalates; only malformed input (an invalid `--dep` pair, a duplicate
193
- `--dep` name, or an invalid `--tests` token) fails with a non-zero exit.
194
- Heads are recorded and compared as full lowercase SHAs. Nothing is persisted and no git operation is performed: the
195
- gate decides readiness only.
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
 
@@ -562,3 +576,14 @@ would-be outcome.
562
576
  - Reporting via `ace-hitl` and resume-after-interruption ship with
563
577
  `ace-coworker` 0.4.0+; role presets resolve their `wfi://` instruction
564
578
  sources through `ace-nav` (runtime dependency since 0.5.0).
579
+
580
+
581
+ For commissioned PR work the reviewer publishes formal approval only after the
582
+ accepted review-round verdict and executed tests, using `ace-git pr approve`.
583
+ Its receipt stays with the existing report. The integrator verifies that receipt
584
+ against read-only `ace-git pr approval` and the current candidate before the
585
+ existing integration gate. A comment, local verdict or draft-ready transition
586
+ does not substitute for provider approval. Accounts and credentials come from
587
+ deployment, independently of the role preset; no identity switching or token
588
+ borrowing is performed by coworker. Missing permission or uncertain publication
589
+ blocks successful integration handoff.
@@ -82,18 +82,36 @@ 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). `blocked` is a
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
94
98
  verdict says accept.
95
99
 
96
- 4. **Merge only a ready candidate.**
100
+ 4. **Consume formal approval, then merge only a ready candidate.**
101
+ - For PR integration, read the reviewer's receipt from the existing report
102
+ and verify current provider evidence without minting another approval:
103
+ `ace-git pr approval PR --expected-head SHA --review-id REVIEW_ID
104
+ --server SERVER --format json` (or the retained `--default-server`). Here
105
+ REVIEW_ID is the numeric provider review ID, not the publication correlation.
106
+ - Require effective `approved` evidence and compare repository, PR, full head,
107
+ reviewer identity, correlation and evidence SHA256 with the accepted
108
+ reviewer report/receipt. The authenticated reviewer must differ from the
109
+ PR author. A stale/dismissed/nonofficial review, comment, unavailable
110
+ evidence or missing account permissions withholds integration. Return to
111
+ the reviewer/overseer; do not issue approval on the reviewer's behalf,
112
+ copy their token or bypass branch protection. This read does not replace
113
+ the dependency/test/current-head gate or authorize a merge by itself.
114
+
97
115
  - Verdict `ready`: perform the project-defined integration with the
98
116
  existing tools (`git` / `ace-git`, scoped to the commissioned paths;
99
117
  never force-push; fetch first and resolve conflicts explicitly).
@@ -162,7 +162,27 @@ product decision is escalated to the overseer instead.
162
162
  outcome from the ace-assign verdict and refuses a forced success on an
163
163
  incomplete assignment - the tool floor, not a courtesy.
164
164
 
165
- 8. **Record and hand off.**
165
+ 8. **Publish formal approval, then hand off.**
166
+ - For a commissioned PR review, only AFTER the review-round verdict is
167
+ `accept`, required executed tests pass, and the accepted report binds the
168
+ current full head, publish through the configured reviewer account:
169
+ `ace-git pr approve PR --expected-head SHA --body-file REVIEW_REPORT
170
+ --correlation REVIEW_ID --server SERVER --format json` (or the retained
171
+ `--default-server` selection). Keep the report and returned receipt in the
172
+ existing task/review/assignment artifacts. Use one stable REVIEW_ID for
173
+ this repository/PR publication; changed report/head needs a new publication.
174
+ - A comment, local acceptance or `pr ready` is not formal approval. Confirm
175
+ the receipt's exact repo/PR/head/reviewer/evidence digest and effective
176
+ approval. Missing permission, unsupported capability or unknown outcome
177
+ blocks successful integration handoff; report the concrete cause via the
178
+ existing HITL/report path. Resume by reconciling the same publication,
179
+ never blindly repeating a POST or switching credentials.
180
+ - Reviewer purpose and provider permissions are independent: deployment
181
+ selects the OS/provider identity. Never borrow worker/admin credentials;
182
+ the PR author cannot approve their own PR. Local-only reviews have no
183
+ remote approval to publish and must not claim one.
184
+
185
+ 8a. **Record and hand off.**
166
186
  - `ace-coworker report` writes the durable record
167
187
  (`<task>/coworker/<assignment>.md`) and delivers the HITL report.
168
188
  - The final report names: the exact candidate head, executed checks,
@@ -209,3 +229,12 @@ and reported. Anything less is an error outcome with the cause named.
209
229
  `.ace-local/review/` to ace-review, `.ace-local/hitl/` to ace-hitl.
210
230
  - Inside the ACE repository itself, invoke every tool as `bin/ace-*` (the
211
231
  binstubs pin the checkout's bundle).
232
+
233
+
234
+ Deployment must provide a dedicated automation reviewer identity and one writer
235
+ per actor/repository/PR before formal publication. Missing evidence of that
236
+ mapping/ownership blocks live use; a role name grants no account permission.
237
+ Do not share that identity with manual clients or another host writer. Preserve
238
+ and report unrelated pending reviews/comments instead of publishing over them.
239
+ Resume only when the provider's path preserves the accepted commit/content;
240
+ unsafe or unsupported pending recovery remains blocked with retained evidence.
@@ -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): the
7
- # integrator merges only a candidate whose (a) dependencies are
8
- # integrated predecessors, (b) current head matches the reviewed
9
- # head (no drift), (c) tests were executed and passed, and (d) a
10
- # fresh independent review verdict accepts the EXACT candidate
11
- # head. Any unknown or contradictory fact is a blocking reason -
12
- # uncertainty must be verified against the real worktree/reports,
13
- # never guessed or retried blind. Explicit :blocked outcome with
14
- # reasons, never an exception. No IO, no writes, no merges: the
15
- # gate decides readiness only; the role drives git/ace-git.
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
- PLACEHOLDERS = %w[task assignment role overseer].freeze
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: [], dep: [],
31
- format: "table", **options)
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, force: false, dry_run: false)
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
- prompt = start_prompt.to_s.strip.empty? ? render_prompt(preset, role, canonical_task, assignment, overseer) : start_prompt
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, "overseer" => overseer})
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?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Coworker
5
- VERSION = "0.6.1"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
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.6.1
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-20 00:00:00.000000000 Z
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
@@ -159,14 +159,14 @@ dependencies:
159
159
  requirements:
160
160
  - - "~>"
161
161
  - !ruby/object:Gem::Version
162
- version: '0.62'
162
+ version: 0.62.2
163
163
  type: :runtime
164
164
  prerelease: false
165
165
  version_requirements: !ruby/object:Gem::Requirement
166
166
  requirements:
167
167
  - - "~>"
168
168
  - !ruby/object:Gem::Version
169
- version: '0.62'
169
+ version: 0.62.2
170
170
  - !ruby/object:Gem::Dependency
171
171
  name: ace-support-test-helpers
172
172
  requirement: !ruby/object:Gem::Requirement
@@ -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