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 +4 -4
- data/CHANGELOG.md +20 -0
- data/handbook/workflow-instructions/overseer/bootstrap-delegation.wf.md +38 -7
- data/handbook/workflow-instructions/overseer/lab-start.wf.md +16 -8
- data/handbook/workflow-instructions/overseer/queue.wf.md +20 -5
- data/lib/ace/overseer/atoms/report_interpretation.rb +28 -14
- data/lib/ace/overseer/molecules/hitl_inbox.rb +1 -0
- data/lib/ace/overseer/molecules/project_bundle.rb +49 -5
- data/lib/ace/overseer/molecules/report_completion_check.rb +52 -0
- data/lib/ace/overseer/organisms/report_processor.rb +15 -2
- data/lib/ace/overseer/organisms/start_orchestrator.rb +6 -1
- data/lib/ace/overseer/version.rb +1 -1
- data/lib/ace/overseer.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d96c79ae10f470e6358022f081234f5080eba56cd58353471a33aa6f187e0c5
|
|
4
|
+
data.tar.gz: 5e4eadc41222d3448b57f938ad5c2716b1941ddfe7b800479d69a6a3f72f3cb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
25
|
-
work.
|
|
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,
|
|
41
|
-
|
|
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.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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.
|
|
166
|
-
step
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
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.
|
|
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 = []
|
|
@@ -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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
data/lib/ace/overseer/version.rb
CHANGED
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.
|
|
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
|
+
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
|