ace-overseer 0.28.0 → 0.28.1

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: c0228801ebed27e7b1fca7fe8f580dc4fdc2a5a5be9e8d76d3c6cf59b1f8cf48
4
- data.tar.gz: 2fae06ba1fc9174fc41f53f3ddcbcc1ee8dddd0152faaaa7e894a862d6f0e8c5
3
+ metadata.gz: 4d96c79ae10f470e6358022f081234f5080eba56cd58353471a33aa6f187e0c5
4
+ data.tar.gz: 5e4eadc41222d3448b57f938ad5c2716b1941ddfe7b800479d69a6a3f72f3cb9
5
5
  SHA512:
6
- metadata.gz: 2ede68b20a4104eeb494014ca34a05627e8367aa5d1b6cf71cbd5f8fcfa0e4168e2029c3ca78be121e353e3e24a25bc0dd0c5ae153850b9797a7bff5bf504e6c
7
- data.tar.gz: 611af27c6c2b7febc096cefeaff5545d8f3ce7fd1dd9e377932371359c850ec0e1f757476d9ffba937135f3ea021bd87f4c268007ab81b0e571c1723b48c4d88
6
+ metadata.gz: f9fdda6024283fd16b8be35da318b7e4eb6a85574ef623cc1395c4c7af0d77fc1971a541bb2a28abe3a6ce7a6d52f1c04df442c69eaff53e09f7d3974b6b89e3
7
+ data.tar.gz: 4fe70abfe425a0c399763c790125f391bbca8099f5a96e4a24677047148fc4b7e1c578ae976853195a28f1e5306500e259de498a491601e241e1c82e1b514c91
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.28.1] - 2026-10-31
11
+
12
+ ### Fixed
13
+
14
+ - Transfer terminal runtime setup through a private script and require its
15
+ acknowledgement before launch, preventing canonical-input PATH truncation.
16
+
17
+ - Bind unassigned readiness only to explicit planner allocations; reject missing
18
+ report identities and conflicting task/role references. Persist bootstrap
19
+ assignment and receipt identities in task metadata, using resolved task paths.
20
+ Check real completed assignment and role ownership before accepting success;
21
+ leave confirm-start continuation to the coordinator without worker dispatch.
22
+
23
+ - Preserve concrete child startup failures reported before project scope is
24
+ established, while continuing to reject unscoped or wrong-scope success.
25
+
26
+ - Delegate bootstrap diagnosis and reproduction to workers as well as repair;
27
+ a known symptom is enough to commission work. Reconcile project startup only
28
+ after accepted worker completion and independent verification.
29
+
10
30
  ## [0.28.0] - 2026-10-31
11
31
 
12
32
  ### Fixed
@@ -10,6 +10,16 @@ purpose: Complete one lab-owned startup commission without the overseer doing th
10
10
 
11
11
  ## Commission and ownership
12
12
 
13
+ The overseer reads owner state and public CLI contracts to establish scope,
14
+ existing execution and the observed symptom. Technical diagnosis, source-code
15
+ investigation, troubleshooting and reproduction commands belong to the worker.
16
+ Record the known failure and desired outcome in the commission; discovering
17
+ the root cause is part of the assigned work, never a prerequisite for dispatch.
18
+ Use existing readiness state, or a startup dry run only when that state is
19
+ otherwise unknown. Do not turn that check into a technical investigation.
20
+ Once the symptom is known, proceed to task creation/reconciliation, observation
21
+ and worker dispatch, then end the turn.
22
+
13
23
  Read existing tasks and assignments first. Reuse the open commission for the
14
24
  same project and goal; unknown execution state never means free capacity.
15
25
  Create one lab coordination task in `_overseer`, with three direct children:
@@ -19,9 +29,12 @@ predecessor. Only the lab overseer updates this private tree.
19
29
  Create a separate executable lab task through `ace-task` for the bootstrap
20
30
  work, linking it as the manager's `project_task`. Keep its specification in
21
31
  its task folder. Include the canonical target repository path, required
22
- configuration, allowed changes, runtime mode, acceptance checks and report
23
- recipient. This is lab infrastructure work, not a project product task.
24
- Workers must not edit the private coordination tree.
32
+ configuration, observed failure, desired outcome, allowed changes, runtime
33
+ mode, acceptance checks and report recipient. Assign diagnosis and reproduction
34
+ as well as preparation to the worker. This is lab infrastructure work, not a project product task.
35
+ Workers must not edit the private coordination tree. Resolve each task folder
36
+ from `ace-task show <ref>` before writing commission, dispatch or receipt notes;
37
+ never construct a folder from a guessed or expanded title slug.
25
38
 
26
39
  Explicit `ACE_TOOLCHAIN=installed` remains installed in every subprocess,
27
40
  worker and resumed session. The project checkout is domain data. Never modify
@@ -44,7 +57,19 @@ report that capability gap. Do not claim that mentioning `/eyes` armed it.
44
57
  Use `ace-coworker prepare --task <executable-task> --roles bootstrap-worker,bootstrap-verifier
45
58
  --to <configured-overseer-address> --format json`. Read its returned worktree,
46
59
  assignment IDs and start-config paths; record these on the matching private
47
- steps before launch. A preparation readiness report is not worker completion.
60
+ steps before launch through the task API, not only in prose:
61
+
62
+ ```bash
63
+ ace-task update <prepare-step> --set assignment=<worker-assignment> --set role=bootstrap-worker --set worktree=<returned-worktree>
64
+ ace-task update <verify-step> --set assignment=<verifier-assignment> --set role=bootstrap-verifier --set worktree=<returned-worktree>
65
+ ace-task update <confirm-step> --set role=confirm-start
66
+ ```
67
+
68
+ Read back the steps with `ace-task show` and verify exact returned identities.
69
+ A preparation readiness report is not worker completion. It does not complete
70
+ any of these three steps; never label bootstrap execution as `role=planner`.
71
+ Only a separate, explicitly commissioned planner preparation allocation may
72
+ accept an unassigned readiness report.
48
73
  The task must be present in the execution worktree before any agent starts.
49
74
 
50
75
  `ace-coworker start --config <returned-path>` validates and prints a brief;
@@ -68,7 +93,13 @@ report ID, task, assignment and role; a preparation report, process exit or
68
93
  unmatched message is not success. Use `ace-overseer process-reports --dry-run`
69
94
  to inspect the matching and continuation before applying it. Only accept
70
95
  worker success with a completed non-empty assignment and the promised evidence.
71
- Record receipt through task APIs before acknowledging the eyes revision.
96
+ Record the actual accepted HITL event ID and result in step metadata through
97
+ `ace-task update <step> --set processed_report=<event-id> --set report_result=<result>`
98
+ (or let `process-reports` perform that same write after its dry run is checked).
99
+ Mark done only after acceptance; errors keep the step open. Receipt notes in
100
+ the path returned by `ace-task show` supplement these fields, never replace them.
101
+ Read back the recorded identity before acknowledging the eyes revision.
102
+ Never put a preparation readiness ID on a worker, verifier or confirm-start step.
72
103
 
73
104
  Use the existing role handoff gate to prepare the independent reviewer after
74
105
  worker completion. As with the worker, a printed start command is not a live
@@ -77,8 +108,8 @@ confirm first-turn delivery. Do not launch a second executor while an earlier
77
108
  one is active or its state is unknown. On repeated delivery, reconcile existing
78
109
  assignments and agents instead of dispatching again.
79
110
 
80
- After accepted independent verification, the overseer performs its coordination
81
- operation `ace-overseer start --reconcile`. Confirm the target's session, first
111
+ Only after accepted worker completion and independent verification does the
112
+ overseer perform this commission's project-start coordination operation `ace-overseer start --reconcile`. Confirm the target's session, first
82
113
  turn and active eyes from actual session evidence. A ready process alone is
83
114
  not initialized observation. Close the confirmation step and managing task
84
115
  only with these proofs; the worker report alone cannot close the family.
@@ -21,8 +21,12 @@ the right project. You never take over a project's tasks, worktrees, or
21
21
  private `_overseer` state — those belong to each project's overseer.
22
22
 
23
23
  You own the OUTCOME of routine startup, never its execution. Configuration,
24
- Bundler installation, dependency repairs and binstub generation are WORKER
25
- work. Create a commission and dispatch a separate lab worker even when the
24
+ Bundler installation, dependency repairs, binstub generation and technical
25
+ diagnosis are WORKER work. Read owner state and public CLI contracts to route
26
+ the commission; source-code investigation, troubleshooting and reproduction
27
+ commands belong to the worker. Capture the observed symptom and desired
28
+ outcome. An unknown root cause is not a reason to postpone task creation and
29
+ dispatch. Create a commission and dispatch a separate lab worker even when the
26
30
  project overseer cannot start yet. Do not edit project configuration, install
27
31
  packages or run repair commands yourself. Missing workers are a concrete
28
32
  capability gap, not a reason to become the worker.
@@ -37,8 +41,8 @@ for the Captain to write YAML. The packaged bootstrap profile is
37
41
  and an invalid explicit configuration never silently falls back.
38
42
 
39
43
  The Captain's latest instruction outranks this guide and stored tasks or plans.
40
- On this first turn, establish your own configuration and the commissioned
41
- project overseers before resuming unrelated stored backlog. Read old
44
+ On this first turn, commission any missing configuration and project
45
+ preparation through workers before resuming unrelated stored backlog. Read old
42
46
  `_overseer` tasks as context; do not dispatch old auth, publication or full
43
47
  acceptance work while the current instruction is bootstrap and project start.
44
48
  Report scope conflicts to the host operator instead of negotiating away a new
@@ -53,10 +57,14 @@ Captain order.
53
57
  eyes configuration path. Check enabled=true, breaker=null, errors=0 and
54
58
  a successful observation before dispatch. A missing tool/configuration is
55
59
  a readiness gap. Writing `/eyes` in prose does not arm observation.
56
- 4. Use `ace-overseer start --reconcile` to inspect/start commissioned project
57
- sessions. Bare `ace-overseer start` launches a foreground harness; never
58
- call it recursively from your own session. Delegate preparation or repair
59
- of blocked projects to a lab worker through the bootstrap procedure.
60
+ 4. For a preparation commission, read existing readiness/session state; use
61
+ a startup dry run only if needed to establish an otherwise unknown status.
62
+ Do not investigate its implementation or reproduce the failure yourself.
63
+ Once the symptom is known, create/reconcile the task and dispatch its
64
+ worker. Run `ace-overseer start --reconcile` for that commission only after
65
+ accepted worker completion and independent verification. Bare
66
+ `ace-overseer start` launches a foreground harness; never call it
67
+ recursively from your own session.
60
68
  5. After dispatch, record the real assignment and agent identity and END YOUR
61
69
  TURN. Do not sleep-poll, run repeated status commands, or wait synchronously
62
70
  for the worker. A semantic change wakes you; unchanged state stays silent.
@@ -80,6 +80,7 @@ task:
80
80
  ```bash
81
81
  ace-task create "Manage: <scope>" --in _overseer --tags queue
82
82
  ace-task create "Prepare <scope>" --child-of <manager-ref>
83
+ ace-task update <prep-ref> --set role=planner
83
84
  ace-task create "Implement <scope>" --child-of <manager-ref>
84
85
  ace-task create "Review <scope>" --child-of <manager-ref>
85
86
  ace-task create "Integrate <scope>" --child-of <manager-ref>
@@ -94,7 +95,11 @@ Rules:
94
95
  - Add worktree/assignment references only after they really exist (the
95
96
  planner prepares them); never fabricate references ahead of preparation.
96
97
  Record them on the step that owns them, e.g.
97
- `ace-task update <step-ref> --set worktree=.ace-wt/<task>/`.
98
+ `ace-task update <step-ref> --set assignment=<actual-id> --set role=<allocated-role> --set worktree=<actual-path>`.
99
+ Read back the fields; notes alone do not bind reports. `role=planner` denotes
100
+ only the allocation that prepares assignments, never bootstrap-worker or
101
+ confirmation of a running project. Resolve note folders from `ace-task show`;
102
+ never guess their title-derived paths.
98
103
 
99
104
  - Cross-task ordering goes through the integration step: a step of task B
100
105
  that requires A's merge depends on A's *integrate* subtask, not on A's
@@ -162,8 +167,9 @@ When a report arrives (ace-hitl event or message):
162
167
  1. Match its message ID plus task/assignment/step references to the current
163
168
  order. A report that fits no open allocation is kept for clarification,
164
169
  not counted as success.
165
- 2. Record the outcome and the processed report ID in the managing task /
166
- step (task-folder notes), then update statuses:
170
+ 2. Persist the exact accepted event ID and result on the owning step:
171
+ `ace-task update <step-ref> --set processed_report=<event-id> --set report_result=<result>`.
172
+ Add evidence notes in the folder returned by `ace-task show`, then update statuses:
167
173
  `ace-task update <step-ref> --set status=done` (or keep open / escalate).
168
174
  3. For success: end the current allocation, then continue with the next
169
175
  unblocked step per rule 3. For a repeated report: check the recorded
@@ -174,14 +180,23 @@ When a report arrives (ace-hitl event or message):
174
180
 
175
181
  `ace-overseer process-reports` executes rules 1-3 mechanically: it matches
176
182
  each incoming event (assignment recorded on a step, or the managing task's
177
- project_task with exactly one non-terminal step), records the outcome and
183
+ project_task with exactly one explicitly allocated `role=planner` non-terminal
184
+ step having no assignment). Task and any supplied role must agree; an event
185
+ without an ID is refused. Unassigned readiness never closes worker, verifier,
186
+ integration or confirm-start allocations. Before recording assigned success, the processor reads the actual coworker role
187
+ allocation and ace-assign state: exact task/assignment/role, a completed nonempty
188
+ plan, and every step done are required in dry-run and apply. Unavailable owner
189
+ state refuses acceptance. It records the outcome and
178
190
  the processed report ID on the step through the ordinary ace-task update
179
191
  surface, and continues with the next unblocked step through the existing
180
192
  gates — `ace-overseer handoff` for the next role when the family records a
181
193
  worktree, otherwise the `work-on` dependency gate. A repeated delivery is
182
194
  answered from the recorded `processed_report` and re-runs nothing; an error
183
195
  report or question never advances a step; a report matching no open
184
- allocation stays for clarification. `--dry-run` interprets without effects.
196
+ allocation stays for clarification. A `role=confirm-start` continuation remains
197
+ a coordinator operation; no generic worker is dispatched and no managing task
198
+ closes until first-turn and observation evidence is accepted. `--dry-run`
199
+ interprets without effects.
185
200
 
186
201
  ### 6) Close
187
202
 
@@ -15,7 +15,7 @@ module Ace
15
15
  # assignment that matches no step is :unmatched), or — only for
16
16
  # reports without an assignment (planner readiness) — by the
17
17
  # managing task's project_task plus exactly one non-terminal
18
- # step whose own assignment is blank (the preparation role). An
18
+ # step explicitly allocated to role planner with no assignment. An
19
19
  # event that fits no open allocation is :unmatched, kept
20
20
  # for clarification, never success (an ambiguous cross-manager
21
21
  # match is also :unmatched).
@@ -45,6 +45,8 @@ module Ace
45
45
  # recorded_result:}
46
46
  # {status: :unmatched, reason:}
47
47
  def self.evaluate(event:, managers:)
48
+ return {status: :unmatched, reason: "event has no report identity"} if event["id"].to_s.strip.empty?
49
+
48
50
  matched = match(event, managers)
49
51
  return {status: :unmatched, reason: unmatched_reason(event)} unless matched
50
52
 
@@ -142,16 +144,11 @@ module Ace
142
144
  # because a stale report for a different assignment must never
143
145
  # complete whichever step happens to remain.
144
146
  #
145
- # Without an assignment (a planner readiness report carries task
146
- # + result only) the report binds to the PREPARATION role alone:
147
- # the managing task's project_task must match the reported task
148
- # AND leave exactly one non-terminal step whose own assignment is
149
- # blank (the preparation step). A sole remaining step that is
150
- # bound to a role assignment belongs to that role, not to the
151
- # report — absence of an assignment is never evidence that
152
- # whichever role remains just finished. A match that is ambiguous
153
- # across managers (or across steps) fits no open allocation and
154
- # returns nil.
147
+ # Unassigned readiness can complete only an explicitly allocated
148
+ # planner step. A missing assignment says nothing about the role:
149
+ # confirm-start and bootstrap execution also have unassigned windows.
150
+ # Task identity and any supplied role must agree with the allocation.
151
+ # Ambiguous allocations never advance.
155
152
  #
156
153
  # @return [Hash{manager:, step:, terminal:}, nil]
157
154
  def self.match(event, managers)
@@ -161,8 +158,10 @@ module Ace
161
158
  assignment = event["assignment"].to_s
162
159
  if assignment != ""
163
160
  matches = managers.flat_map do |manager|
161
+ next [] unless task_agrees?(event, manager)
162
+
164
163
  steps_of(manager).filter_map do |step|
165
- {manager: manager, step: step} if step.metadata["assignment"].to_s == assignment
164
+ {manager: manager, step: step} if step.metadata["assignment"].to_s == assignment && role_agrees?(event, step)
166
165
  end
167
166
  end
168
167
  # No step records this assignment: unmatched, never a
@@ -182,7 +181,9 @@ module Ace
182
181
  next unless manager.metadata["project_task"].to_s == task && task != ""
183
182
 
184
183
  active = steps_of(manager).reject { |step| terminal?(step) }
185
- preparation = active.reject { |step| step.metadata["assignment"].to_s != "" }
184
+ preparation = active.select do |step|
185
+ step.metadata["assignment"].to_s.empty? && step.metadata["role"] == "planner" && role_agrees?(event, step)
186
+ end
186
187
  {manager: manager, step: preparation.first} if preparation.size == 1
187
188
  end
188
189
  return nil if matches.size > 1
@@ -201,7 +202,9 @@ module Ace
201
202
  def self.recorded_match(event, managers)
202
203
  matches = managers.flat_map do |manager|
203
204
  steps_of(manager).filter_map do |step|
204
- {manager: manager, step: step} if step.metadata[PROCESSED_REPORT_KEY] == event["id"]
205
+ {manager: manager, step: step} if step.metadata[PROCESSED_REPORT_KEY] == event["id"] &&
206
+ task_agrees?(event, manager) && role_agrees?(event, step) &&
207
+ (event["assignment"].to_s.empty? || step.metadata["assignment"].to_s == event["assignment"].to_s)
205
208
  end
206
209
  end
207
210
  return nil if matches.size != 1
@@ -210,6 +213,17 @@ module Ace
210
213
  terminal: terminal?(matches.first[:step])}
211
214
  end
212
215
 
216
+ def self.task_agrees?(event, manager)
217
+ expected = manager.metadata["project_task"].to_s
218
+ actual = event["task"].to_s
219
+ expected.empty? || expected == actual
220
+ end
221
+
222
+ def self.role_agrees?(event, step)
223
+ role = event["role"].to_s
224
+ role.empty? || step.metadata["role"].to_s == role
225
+ end
226
+
213
227
  # @return [String] why the event fit no open allocation
214
228
  def self.unmatched_reason(event)
215
229
  refs = []
@@ -34,6 +34,7 @@ module Ace
34
34
  "result" => event.result,
35
35
  "task" => event.metadata["task"],
36
36
  "assignment" => event.metadata["assignment"],
37
+ "role" => event.metadata["role"],
37
38
  "title" => event.title
38
39
  }
39
40
  end
@@ -3,6 +3,9 @@
3
3
  require "json"
4
4
  require "ace/herdr/molecules/bounded_process"
5
5
  require "shellwords"
6
+ require "tempfile"
7
+ require "securerandom"
8
+ require "fileutils"
6
9
  require "ace/support/cli/toolchain"
7
10
 
8
11
  module Ace
@@ -59,19 +62,53 @@ module Ace
59
62
  end
60
63
 
61
64
  def self.shell_setup(root, env: ENV)
65
+ values = environment(root, env: env).reject { |name, _| %w[HERDR_PANE HERDR_TAB HERDR_PANE_ID HERDR_TAB_ID].include?(name) }
62
66
  if Ace::Support::Cli::Toolchain.installed?
63
67
  # A server-created shell may contain selectors absent from this
64
68
  # caller. Clear the same known selectors before applying the
65
69
  # installed closure; never inherit server-side Bundler injection.
66
70
  names = BUNDLE_SELECTORS + BUNDLE_SELECTORS.map { |key| "BUNDLER_ORIG_#{key}" }
67
- return "unset #{names.map { |name| Shellwords.escape(name) }.join(' ')}; " +
68
- Ace::Support::Cli::Toolchain.shell_setup
71
+ values = names.to_h { |name| [name, nil] }.merge(values)
69
72
  end
70
73
 
71
- values = environment(root, env: env).reject { |name, _| %w[HERDR_PANE HERDR_TAB HERDR_PANE_ID HERDR_TAB_ID].include?(name) }
72
74
  cleared, assigned = values.partition { |_, value| value.nil? }
73
- "unset #{cleared.map { |name, _| Shellwords.escape(name) }.join(" ")}; export " +
74
- assigned.map { |name, value| "#{name}=#{Shellwords.escape(value)}" }.join(" ")
75
+ # Guard every operation: e.g. a readonly inherited selector must
76
+ # refuse setup rather than print readiness after a failed unset.
77
+ commands = cleared.map { |name, _| "unset #{Shellwords.escape(name)}" } +
78
+ assigned.map { |name, value| "export #{name}=#{Shellwords.escape(value)}" }
79
+ commands.join(" &&\n")
80
+ end
81
+
82
+ # pane_run types through a terminal: Linux canonical input truncates
83
+ # a line after 4095 bytes. Keep the environment in a private shared
84
+ # project file, not /tmp (Herdr can use a different PrivateTmp).
85
+ # The caller must observe the marker before launching the harness.
86
+ def self.with_shell_setup(root, env: ENV)
87
+ directory = root
88
+ %w[.ace-local overseer runtime-start].each do |part|
89
+ directory = File.join(directory, part)
90
+ raise Ace::Overseer::Error, "runtime setup directory is a symlink: #{directory}" if File.symlink?(directory)
91
+
92
+ Dir.mkdir(directory, 0o700) unless File.exist?(directory)
93
+ raise Ace::Overseer::Error, "runtime setup path is not a directory: #{directory}" unless File.directory?(directory)
94
+ end
95
+ info = File.stat(directory)
96
+ unless info.uid == Process.uid && (info.mode & 0o077).zero?
97
+ raise Ace::Overseer::Error, "runtime setup directory must be private and owned by the current user: #{directory}"
98
+ end
99
+ marker = "ACE_OVERSEER_ENV_READY_#{SecureRandom.hex(16)}"
100
+ Tempfile.create(["environment-", ".sh"], directory) do |file|
101
+ setup = shell_setup(root, env: env).strip.sub(/;\z/, "")
102
+ file.write("#{setup} && printf '%s\\n' #{Shellwords.escape(marker)}\n")
103
+ file.flush
104
+ file.fsync
105
+ # The tab is created in root. An explicit ./ prevents dot from
106
+ # searching PATH; a relative path also keeps terminal input short.
107
+ relative = "./.ace-local/overseer/runtime-start/#{File.basename(file.path)}"
108
+ yield ". #{Shellwords.escape(relative)}", marker
109
+ end
110
+ rescue SystemCallError => e
111
+ raise Ace::Overseer::Error, "runtime setup file could not be prepared: #{e.message}"
75
112
  end
76
113
 
77
114
  def initialize(timeout_s: 240)
@@ -97,6 +134,13 @@ module Ace
97
134
  raise Ace::Overseer::Error, "project start output exceeded its limit" if result.oversized
98
135
  out, err, status = result.stdout, result.stderr, result.status
99
136
  report = JSON.parse(out)
137
+ # Startup can fail before it establishes project scope. Keep that
138
+ # concrete failure without admitting an unscoped report as ready.
139
+ if report.is_a?(Hash) && report["status"] == "blocked" &&
140
+ report.key?("scope") && report["scope"].nil? &&
141
+ report["message"].is_a?(String) && !report["message"].strip.empty?
142
+ raise Ace::Overseer::Error, report["message"]
143
+ end
100
144
  unless report.is_a?(Hash) && report["scope"] == "project" && report["status"].is_a?(String)
101
145
  raise Ace::Overseer::Error, "project start returned no project-scope report"
102
146
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Overseer
5
+ module Molecules
6
+ # Read existing coworker allocation and ace-assign completion truth before
7
+ # the queue accepts success. This check owns no persisted state.
8
+ class ReportCompletionCheck
9
+ def initialize(handoff_probe: nil, assignment_probe_factory: nil, worktree_finder: nil)
10
+ @worktree_finder = worktree_finder || Ace::Git::Worktree::Organisms::WorktreeManager.new
11
+ @handoff_probe = handoff_probe || HandoffProbe.new
12
+ @assignment_probe_factory = assignment_probe_factory || ->(root) {
13
+ Ace::Coworker::Molecules::AssignmentProbe.new(project_root: root)
14
+ }
15
+ end
16
+
17
+ def call(event:, manager:, step:)
18
+ return "matched task allocation no longer exists" unless manager && step
19
+ assignment = event["assignment"].to_s
20
+ if assignment.empty?
21
+ return nil if step.metadata["role"] == "planner" && step.metadata["assignment"].to_s.empty?
22
+ return "unassigned readiness is not execution completion"
23
+ end
24
+ task = manager.metadata["project_task"].to_s
25
+ role = step.metadata["role"].to_s
26
+ worktree = step.metadata["worktree"].to_s
27
+ worktree = manager.metadata["worktree"].to_s if worktree.empty?
28
+ return "success requires recorded project task, role and worktree" if [task, role, worktree].any?(&:empty?)
29
+ return "report task disagrees with the allocation" unless event["task"].to_s == task
30
+ return "confirm-start requires coordinator session evidence" if role == "confirm-start"
31
+ return "report assignment disagrees with the allocation" unless step.metadata["assignment"].to_s == assignment
32
+
33
+ found = @worktree_finder.switch(task)
34
+ return "task has no usable worktree: #{found[:error] || found[:message]}" unless found[:success]
35
+ root = found.fetch(:worktree_path)
36
+ legs = @handoff_probe.probe(root, task_id: task).fetch(:legs)
37
+ matching = legs.select { |leg| leg["assignment"] == assignment }
38
+ return "assignment has no unique allocation for this task" unless matching.size == 1
39
+ return "assignment role disagrees with the recorded allocation" unless matching.first["role"] == role
40
+ return "report role disagrees with the allocation" unless event["role"].to_s.empty? || event["role"] == role
41
+
42
+ snapshot = @assignment_probe_factory.call(root).snapshot(assignment)
43
+ return "assignment owner returned a different identity" unless snapshot.id == assignment
44
+ return "assignment is not complete: #{snapshot.incomplete_reasons.join('; ')}" unless snapshot.completion_verdict == :complete
45
+ nil
46
+ rescue Ace::Coworker::Error, Ace::Overseer::Error, SystemCallError, KeyError, Psych::Exception => e
47
+ "assignment completion could not be verified in #{worktree}: #{e.message}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -16,7 +16,8 @@ module Ace
16
16
  class ReportProcessor
17
17
  OUTCOME_STATUSES = %i[success error needs_answer duplicate unmatched].freeze
18
18
 
19
- def initialize(inbox: nil, task_manager: nil, handoff: nil, work_on: nil)
19
+ def initialize(inbox: nil, task_manager: nil, handoff: nil, work_on: nil, completion_check: nil)
20
+ @completion_check = completion_check || Molecules::ReportCompletionCheck.new
20
21
  @inbox = inbox || Molecules::HitlInbox.new
21
22
  @task_manager = task_manager || Ace::Task::Organisms::TaskManager.new
22
23
  @handoff = handoff || Organisms::HandoffOrchestrator.new(task_loader: @task_manager)
@@ -45,6 +46,12 @@ module Ace
45
46
  def process_event(event, dry_run:, progress:)
46
47
  verdict = Atoms::ReportInterpretation.evaluate(event: event, managers: private_managers)
47
48
 
49
+ if verdict[:status] == :success || (verdict[:status] == :duplicate && verdict[:recorded_result] == "success")
50
+ refusal = @completion_check.call(event: event,
51
+ manager: @task_manager.show(verdict[:manager_ref]), step: @task_manager.show(verdict[:step_ref]))
52
+ verdict = {status: :unmatched, reason: refusal} if refusal
53
+ end
54
+
48
55
  case verdict[:status]
49
56
  when :unmatched
50
57
  progress.call("Event #{event['id']}: #{verdict[:reason]}")
@@ -135,6 +142,11 @@ module Ace
135
142
  return {action: "none", reason: reason}
136
143
  end
137
144
 
145
+ if next_step.metadata["role"] == "confirm-start"
146
+ return {action: "coordinator", step_ref: next_step.id.to_s,
147
+ reason: "confirm project first turn and active eyes through the standard start operation; keep this step and manager open until proven"}
148
+ end
149
+
138
150
  begin
139
151
  if family_worktree(manager)
140
152
  result = @handoff.call(task_ref: manager.metadata["project_task"].to_s)
@@ -217,7 +229,7 @@ module Ace
217
229
  case continuation[:action]
218
230
  when "handoff"
219
231
  if continuation[:status] == "sequence_complete"
220
- "sequence complete - the worktree has no next role; close the managing task per the queue workflow"
232
+ "sequence complete - the worktree has no next role; pending coordination steps still require acceptance before closing the managing task"
221
233
  elsif continuation[:start_command]
222
234
  "window ensured in #{continuation[:worktree_path]} for the next role " \
223
235
  "(#{continuation.dig(:next, 'role')}); run there: #{continuation[:start_command]}"
@@ -231,6 +243,7 @@ module Ace
231
243
  else
232
244
  "next step #{continuation[:step_ref]} started via work-on"
233
245
  end
246
+ when "coordinator" then "coordinator action for #{continuation[:step_ref]}: #{continuation[:reason]}"
234
247
  when "blocked" then "continuation blocked: #{continuation[:reason]}"
235
248
  when "none" then continuation[:reason]
236
249
  end
@@ -576,7 +576,10 @@ module Ace
576
576
  "herdr created tab '#{label}' but reported no root pane"
577
577
  end
578
578
 
579
- @executor.pane_run(pane, Molecules::ProjectBundle.shell_setup(root, env: @env))
579
+ Molecules::ProjectBundle.with_shell_setup(root, env: @env) do |command, marker|
580
+ @executor.pane_run(pane, command)
581
+ @executor.pane_wait_output(pane, pattern: marker, timeout_ms: START_TIMEOUT_MS)
582
+ end
580
583
  args = ["--model", plan["profile"]["model"]]
581
584
  if plan["profile"]["thinking"]
582
585
  args += ["--thinking", plan["profile"]["thinking"]]
@@ -586,6 +589,8 @@ module Ace
586
589
  @executor.agent_start(name: AGENT_NAME, kind: plan["profile"]["harness"],
587
590
  pane: pane, timeout_ms: START_TIMEOUT_MS, args: args)
588
591
  pane
592
+ rescue Ace::Herdr::WaitTimeoutError => e
593
+ raise Ace::Overseer::Error, "runtime setup was not confirmed; agent not started: #{e.message}"
589
594
  rescue Ace::Herdr::CommandError => e
590
595
  raise Ace::Overseer::Error, "herdr failed to start the overseer session: #{e.message}"
591
596
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Overseer
5
- VERSION = "0.28.0"
5
+ VERSION = "0.28.1"
6
6
  end
7
7
  end
data/lib/ace/overseer.rb CHANGED
@@ -36,6 +36,7 @@ require_relative "overseer/molecules/assignment_launcher"
36
36
  require_relative "overseer/molecules/worktree_context_collector"
37
37
  require_relative "overseer/molecules/handoff_probe"
38
38
  require_relative "overseer/molecules/hitl_inbox"
39
+ require_relative "overseer/molecules/report_completion_check"
39
40
  require_relative "overseer/molecules/local_sources"
40
41
  require_relative "overseer/molecules/remote_sources"
41
42
  require_relative "overseer/molecules/event_observation"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-overseer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.0
4
+ version: 0.28.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-31 00:00:00.000000000 Z
10
+ date: 2026-11-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-cli
@@ -353,6 +353,7 @@ files:
353
353
  - lib/ace/overseer/molecules/project_root_resolver.rb
354
354
  - lib/ace/overseer/molecules/prune_safety_checker.rb
355
355
  - lib/ace/overseer/molecules/remote_sources.rb
356
+ - lib/ace/overseer/molecules/report_completion_check.rb
356
357
  - lib/ace/overseer/molecules/window_opener.rb
357
358
  - lib/ace/overseer/molecules/worktree_context_collector.rb
358
359
  - lib/ace/overseer/molecules/worktree_provisioner.rb