ace-overseer 0.17.0 → 0.18.2
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 +34 -10
- data/docs/usage.md +41 -5
- data/handbook/workflow-instructions/overseer.wf.md +33 -48
- data/lib/ace/overseer/cli/commands/prune.rb +140 -22
- data/lib/ace/overseer/models/assignment_prune_candidate.rb +7 -3
- data/lib/ace/overseer/models/preservation_proof.rb +37 -0
- data/lib/ace/overseer/models/prune_candidate.rb +11 -3
- data/lib/ace/overseer/molecules/assignment_prune_safety_checker.rb +94 -1
- data/lib/ace/overseer/molecules/git_preservation_checker.rb +314 -0
- data/lib/ace/overseer/molecules/lab_client.rb +21 -0
- data/lib/ace/overseer/molecules/lab_prune_safety_checker.rb +69 -0
- data/lib/ace/overseer/molecules/preservation_manifest.rb +231 -0
- data/lib/ace/overseer/molecules/prune_safety_checker.rb +159 -16
- data/lib/ace/overseer/organisms/prune_orchestrator.rb +269 -67
- data/lib/ace/overseer/organisms/work_on_orchestrator.rb +64 -46
- data/lib/ace/overseer/version.rb +1 -1
- data/lib/ace/overseer.rb +10 -2
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3fe6826e192a1bb720405c3b53d7fa5f61faeb3bf382ecdef6bfddcc3497c69
|
|
4
|
+
data.tar.gz: e46d2230ef2aac4b5e3c6eea8b3545d25efcdcc9b9481220a2efb6c922ff6102
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b02701a464f6717a60095b7c29e4e05fa86a80a96885805c378a6fd7d26b265badc57ba17d318a7142364571729916edf15f23ac61bc66c2a96fd10ddf0a01a
|
|
7
|
+
data.tar.gz: 533d1502fc901da26569f44aa31e085489b639eac47418dba12b9f97e3987fee7fbb6d572dd8ff3ff57908accbc0f1355ab76a5bf043dc52cad880d8a641aea4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.18.2] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Technical
|
|
13
|
+
- Follow the ace-assign 0.61 release line for journal-backed service effects.
|
|
14
|
+
- Updated the ace-tmux dependency to the 0.18 adapter release line.
|
|
15
|
+
|
|
16
|
+
## [0.18.1] - 2026-10-01
|
|
17
|
+
|
|
18
|
+
### Technical
|
|
19
|
+
- Follow the ace-assign 0.60 release line for durable review campaign evidence.
|
|
20
|
+
|
|
21
|
+
## [0.18.0] - 2026-09-29
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Preservation manifest for prune: `ace-overseer prune --preservation FILE` accepts a strict `version: 1` YAML manifest declaring cross-repository destinations (source repo/base/head, destination repo/base/head/branch); entries are resolved to exact commits, and duplicate, self-destination, or unmatched entries fail before any apply action.
|
|
25
|
+
- Git preservation proofs on the actual prune path: accepted-ancestor containment against the surviving base branch, or verified-destination proof by full-tree equality or exact path-by-path transition (paths, modes, symlinks, binary content) across separate bases. Patch-range proofs require the independently recorded attempt baseline (`base_head`), and the baseline itself must survive on an accepted ref; empty or truncated ranges are never proof.
|
|
26
|
+
- Fail-closed Lab prune classification: `ace-overseer prune WORK --runtime lab` requires an authoritative terminal Work state plus preservation data (`repo`/`head`/`branch`) provable in the hosted repository; apply re-reads state immediately before `lab work destroy` and blocked Works are never destroyed.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- BREAKING: `--force` no longer removes unsafe worktrees; it only skips the confirmation for already-safe candidates. Missing, failed or ambiguous preservation evidence, dirty tracked or untracked files, active or uncertain attempts, unreadable lifecycle state, and failed rechecks now always block deletion -- including under `--yes` and `--quiet`.
|
|
30
|
+
- Prune preview is read-only (no stale-metadata mutation); apply holds a durable lifecycle exclusion shared with assignment/driver/work-on start paths from final evidence reads through removal, deletes the worktree without force or untracked suppression, and deletes the branch only after re-verifying its tip identity. Removal is recorded so later starts fail closed instead of recreating a deleted identity.
|
|
31
|
+
- Assignment cache prune requires the durable evidence ref (`refs/ace/execution`, never deleted) to confirm every attempt terminal for managed assignments; only the assignment cache directory is removed.
|
|
32
|
+
- Unmatched explicit prune targets are errors, apply exits nonzero when any candidate is blocked or removal fails (independent safe candidates still complete), quiet mode suppresses progress but never failure signaling, and dry-run performs the same proof classification with no cleanup side effects.
|
|
33
|
+
|
|
10
34
|
## [0.17.0] - 2026-09-29
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -380,7 +404,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
380
404
|
## [0.4.17] - 2026-03-02
|
|
381
405
|
|
|
382
406
|
### Changed
|
|
383
|
-
- Replace `ace-taskflow` dependency with `ace-task`
|
|
407
|
+
- Replace `ace-taskflow` dependency with `ace-task` -- migrate `WorkOnOrchestrator` and `PruneSafetyChecker` to use `Ace::Task::Organisms::TaskManager` API
|
|
384
408
|
- Remove bare `require "ace/taskflow"` import, add `require "ace/task"`
|
|
385
409
|
|
|
386
410
|
## [0.4.16] - 2026-02-26
|
|
@@ -470,14 +494,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
470
494
|
## [0.4.2] - 2026-02-19
|
|
471
495
|
|
|
472
496
|
### Added
|
|
473
|
-
- Orchestrator subtask expansion for `work-on-tasks` preset
|
|
497
|
+
- Orchestrator subtask expansion for `work-on-tasks` preset -- when a task is an orchestrator with subtasks, `AssignmentLauncher` expands subtask refs into individual foreach steps (e.g., `work-on-272.01`, `work-on-272.02`) instead of a single `work-on-272` step
|
|
474
498
|
- `extract_subtask_refs` helper in `WorkOnOrchestrator` to extract subtask numbers from orchestrator task data
|
|
475
499
|
- `subtask_refs:` keyword argument on `AssignmentLauncher#launch` for passing expanded subtask references
|
|
476
500
|
|
|
477
501
|
## [0.4.1] - 2026-02-19
|
|
478
502
|
|
|
479
503
|
### Added
|
|
480
|
-
- `--watch` / `-w` option for `status` command
|
|
504
|
+
- `--watch` / `-w` option for `status` command -- auto-refreshing dashboard with ANSI screen clear
|
|
481
505
|
- Two-tier refresh: fast interval (15s default) refreshes assignment data only, slow interval (5min default) does full git/PR refresh
|
|
482
506
|
- `collect_assignments_only` method on `WorktreeContextCollector` for lightweight assignment-only collection with cached git data
|
|
483
507
|
- `collect_quick` method on `StatusCollector` to reuse previous snapshot's git data while refreshing assignments
|
|
@@ -487,7 +511,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
487
511
|
## [0.4.0] - 2026-02-19
|
|
488
512
|
|
|
489
513
|
### Added
|
|
490
|
-
- Progress bar visualization in assignment sub-rows
|
|
514
|
+
- Progress bar visualization in assignment sub-rows -- filled/empty bar segments alongside numeric counts
|
|
491
515
|
- Current step name display for running assignments (e.g., `implement` shown dimmed after progress)
|
|
492
516
|
- Header row and separator line above hierarchical dashboard for column labeling
|
|
493
517
|
- Blank line separators between location groups for visual breathing room
|
|
@@ -499,7 +523,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
499
523
|
## [0.3.1] - 2026-02-19
|
|
500
524
|
|
|
501
525
|
### Changed
|
|
502
|
-
- Hierarchical status display
|
|
526
|
+
- Hierarchical status display -- location header rows with assignment sub-rows replace flat single-row format
|
|
503
527
|
- `WorkContext` model uses `assignments` array instead of singular `assignment_status` + `assignment_count`
|
|
504
528
|
- `WorktreeContextCollector` loads all assignments via `AssignmentDiscoverer` instead of only active via `AssignmentExecutor`
|
|
505
529
|
- `StatusFormatter` emits two row types: location header (basename + PR + Git) and indented assignment sub-rows (ID + name + state + progress)
|
|
@@ -513,7 +537,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
513
537
|
## [0.3.0] - 2026-02-19
|
|
514
538
|
|
|
515
539
|
### Added
|
|
516
|
-
- Assignment-aware status display
|
|
540
|
+
- Assignment-aware status display -- main branch appears in `status` when it has active assignments
|
|
517
541
|
- Assignment count shown in Assign column when location has multiple assignments (e.g., `abc12 (3)`)
|
|
518
542
|
- `--assignment` / `-a` option for `prune` command to remove a specific assignment's cache directory
|
|
519
543
|
- `AssignmentPruneCandidate` model for assignment-level prune safety checking
|
|
@@ -533,16 +557,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
533
557
|
|
|
534
558
|
### Changed
|
|
535
559
|
|
|
536
|
-
- `TmuxWindowOpener` delegates entirely to `ace-tmux window` CLI
|
|
560
|
+
- `TmuxWindowOpener` delegates entirely to `ace-tmux window` CLI -- no longer manages session names, window names, or presets
|
|
537
561
|
- Remove `tmux_session_name`, `window_name_format`, and `window_preset` config options
|
|
538
|
-
- Remove `WindowNameFormatter` atom
|
|
562
|
+
- Remove `WindowNameFormatter` atom -- window naming is ace-tmux's responsibility
|
|
539
563
|
- `PruneOrchestrator` uses worktree path basename for window cleanup instead of formatted names
|
|
540
564
|
|
|
541
565
|
## [0.2.16] - 2026-02-19
|
|
542
566
|
|
|
543
567
|
### Changed
|
|
544
568
|
|
|
545
|
-
- `TmuxWindowOpener` no longer manages tmux sessions
|
|
569
|
+
- `TmuxWindowOpener` no longer manages tmux sessions -- delegates entirely to ace-tmux `WindowManager` for window creation, session detection, and dedup
|
|
546
570
|
|
|
547
571
|
## [0.2.15] - 2026-02-19
|
|
548
572
|
|
|
@@ -559,7 +583,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
559
583
|
## [0.2.13] - 2026-02-19
|
|
560
584
|
|
|
561
585
|
### Added
|
|
562
|
-
- Progress callbacks (`on_progress:`) for `work-on` and `prune` orchestrators
|
|
586
|
+
- Progress callbacks (`on_progress:`) for `work-on` and `prune` orchestrators -- one-line status output per step
|
|
563
587
|
- Prune now displays safe/skipped candidates with reasons before the "Continue?" confirmation prompt
|
|
564
588
|
|
|
565
589
|
### Changed
|
data/docs/usage.md
CHANGED
|
@@ -69,14 +69,31 @@ Arguments:
|
|
|
69
69
|
Options:
|
|
70
70
|
|
|
71
71
|
- `--assignment`, `-a`: prune a specific assignment by ID
|
|
72
|
-
- `--force`, `-f`:
|
|
72
|
+
- `--force`, `-f`: skip the interactive confirmation for already-safe candidates; never bypasses preservation, dirtiness, lifecycle or revalidation blocks
|
|
73
73
|
- `--yes`, `-y`: skip interactive confirmation
|
|
74
|
-
- `--dry-run`:
|
|
75
|
-
- `--
|
|
76
|
-
- `--
|
|
74
|
+
- `--dry-run`: run the same proof classification as apply, changing no worktrees, refs, assignment state or metadata (a dry-run receipt is not reusable authorization)
|
|
75
|
+
- `--preservation FILE`: YAML manifest (`version: 1`, `candidates:`) declaring cross-repository destinations for migrated work; a claim to verify, never proof by itself
|
|
76
|
+
- `--runtime`: `tmux` (default) or `lab`
|
|
77
|
+
- `--quiet`, `-q`: suppress progress output; blocked/failed results still print and signal through the exit code
|
|
77
78
|
- `--debug`, `-d`: show debug output
|
|
78
79
|
- `--help`, `-h`: show help
|
|
79
80
|
|
|
81
|
+
### Prune safety (enforced)
|
|
82
|
+
|
|
83
|
+
Every candidate is deleted only after executed proofs; missing, failed or
|
|
84
|
+
ambiguous evidence preserves the candidate, and `--force`/`--yes` never
|
|
85
|
+
override a block:
|
|
86
|
+
|
|
87
|
+
- Preservation: source HEAD is contained in the surviving accepted base branch, or a declared destination is verified -- accepted on its surviving destination branch -- with full-tree equality or an exact content transition (paths, modes, symlinks, binary content) across separate bases. Patch-range proofs additionally require the independently recorded attempt baseline, and that baseline itself must survive on an accepted ref.
|
|
88
|
+
|
|
89
|
+
- No active writer: recorded attempts must be terminal (active or uncertain attempts block; unreadable attempt state blocks); assignment/driver/start paths and prune share a durable exclusion that survives removal of the target, so no second writer can enter during deletion; dirty tracked or untracked files are work and block deletion, even with `--force`.
|
|
90
|
+
|
|
91
|
+
- Assignment cache: cleanup removes only the assignment cache directory after the durable journal evidence (`refs/ace/execution`, never deleted) confirms every attempt terminal; the journal checkout is never removed.
|
|
92
|
+
|
|
93
|
+
- Exit codes: dry-run always exits 0 (blocked candidates are listed); apply exits nonzero when any selected candidate is blocked or removal fails, while independent safe candidates still complete.
|
|
94
|
+
|
|
95
|
+
- Unmatched explicit targets are errors; empty automatic selection is a no-op; cancelled confirmation deletes nothing.
|
|
96
|
+
|
|
80
97
|
## Example Flows
|
|
81
98
|
|
|
82
99
|
Start task work: `ace-overseer work-on --task 8q4.t.umu.1`.
|
|
@@ -85,6 +102,25 @@ Check dashboard: `ace-overseer status`.
|
|
|
85
102
|
|
|
86
103
|
Preview then prune: `ace-overseer prune --dry-run`, then `ace-overseer prune --yes`.
|
|
87
104
|
|
|
105
|
+
Prune migrated work with a declared destination:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
mkdir -p .ace-local/prune
|
|
109
|
+
cat > .ace-local/prune/destinations.yml <<'YAML'
|
|
110
|
+
version: 1
|
|
111
|
+
candidates:
|
|
112
|
+
- worktree_path: /abs/path/.ace-wt/task.230
|
|
113
|
+
source_repo: /abs/path
|
|
114
|
+
source_base: <base-sha>
|
|
115
|
+
source_head: <head-sha>
|
|
116
|
+
destination_repo: /abs/successor
|
|
117
|
+
destination_base: <dest-base-sha>
|
|
118
|
+
destination_head: <accepted-head-sha>
|
|
119
|
+
destination_branch: refs/heads/main
|
|
120
|
+
YAML
|
|
121
|
+
ace-overseer prune task.230 --preservation .ace-local/prune/destinations.yml --dry-run
|
|
122
|
+
```
|
|
123
|
+
|
|
88
124
|
## Lab Runtime
|
|
89
125
|
|
|
90
126
|
Lab commands are available only where `/usr/local/bin/lab` is installed. ACE
|
|
@@ -98,7 +134,7 @@ does not read Lab credentials and does not call Podman or Herdr directly.
|
|
|
98
134
|
- `ace-overseer prompt --work WORK --file PATH`: forward prompt text from a file to the Work pane. Piped stdin is also supported; prompt text is never passed as a process argument.
|
|
99
135
|
- `ace-overseer review --work WORK --pr NUMBER`: prepare an exact-head admin review checkout and pane.
|
|
100
136
|
- `ace-overseer stop --work WORK`: stop the assigned process without destroying Work state.
|
|
101
|
-
- `ace-overseer prune WORK... --runtime lab --dry-run`:
|
|
137
|
+
- `ace-overseer prune WORK... --runtime lab --dry-run`: classify each Work -- documented terminal state, no in-flight work, and preservation data (`repo`/`head`/`branch`) provable in the hosted repository. Apply with `--yes` re-verifies state immediately before delegating each destruction to `lab work destroy WORK --confirm`; blocked Works are never destroyed and the run exits nonzero. The raw Lab CLI cannot make the state check and the destruction atomic, so this adapter reports the path unsupported and preserves the Work; only a Lab surface with an atomic guarded destroy delegates.
|
|
102
138
|
|
|
103
139
|
Example:
|
|
104
140
|
|
|
@@ -6,10 +6,9 @@ doc-type: workflow
|
|
|
6
6
|
title: Overseer Workflow
|
|
7
7
|
purpose: Binding process contract for the overseer lifecycle (work-on, status, prune) with executed-check contracts for prune safety and status truth.
|
|
8
8
|
ace-docs:
|
|
9
|
-
last-updated: '2026-09-
|
|
10
|
-
last-checked: '2026-09-
|
|
9
|
+
last-updated: '2026-09-29'
|
|
10
|
+
last-checked: '2026-09-29'
|
|
11
11
|
---
|
|
12
|
-
|
|
13
12
|
# Overseer Workflow
|
|
14
13
|
|
|
15
14
|
## Goal
|
|
@@ -79,7 +78,7 @@ ace-overseer prune --yes
|
|
|
79
78
|
```
|
|
80
79
|
|
|
81
80
|
- Targeted: `ace-overseer prune <task-ref|folder>...` or `ace-overseer prune --assignment <assignment-id>`
|
|
82
|
-
- Lab runtime: `ace-overseer prune <work-id>... --runtime lab --dry-run`, rerun with `--yes` to delegate each destruction to Lab.
|
|
81
|
+
- Lab runtime: `ace-overseer prune <work-id>... --runtime lab --dry-run`, rerun with `--yes` to delegate each destruction to Lab. The raw Lab CLI cannot make the no-writer check and the destruction atomic, so it is classified unsupported and preserved; only a Lab surface with an atomic guarded destroy delegates.
|
|
83
82
|
|
|
84
83
|
#### Prune safety (non-negotiable executed check)
|
|
85
84
|
|
|
@@ -101,43 +100,25 @@ candidate:
|
|
|
101
100
|
plus content equivalence. A matching commit subject is never sufficient
|
|
102
101
|
proof:
|
|
103
102
|
|
|
104
|
-
1. Declare the destination concretely
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
test "$(git -C <successor-repo> rev-parse '<dest-ref>^{tree}')" \
|
|
124
|
-
= "$(git rev-parse '<work-head>^{tree}')"
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
- Patch equivalence -- the same changes, compared inside the
|
|
128
|
-
successor repository. Fetch the source proof refs first and
|
|
129
|
-
range-diff with separate source and destination bases:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
git -C <successor-repo> fetch <source-repo> <source-base> <work-head>
|
|
133
|
-
git -C <successor-repo> range-diff \
|
|
134
|
-
<source-base>...<work-head> <dest-base>...<dest-ref>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
with no substantive differences; or diff the normalized patches
|
|
138
|
-
(`git show --format=` output of each side) and require an empty
|
|
139
|
-
diff.
|
|
140
|
-
|
|
103
|
+
1. Declare the destination concretely in a preservation manifest
|
|
104
|
+
(`--preservation FILE`, schema in `docs/usage.md`): successor
|
|
105
|
+
repository, separate destination base/head, and the surviving
|
|
106
|
+
destination branch. The manifest is a claim to verify -- never
|
|
107
|
+
authorization or proof by itself.
|
|
108
|
+
2. Run `ace-overseer prune <target> --preservation FILE --dry-run`. The
|
|
109
|
+
tool executes the proof in this session: it verifies the declared
|
|
110
|
+
source identity against the live worktree (repo and current HEAD),
|
|
111
|
+
verifies the destination base/head are accepted on the surviving
|
|
112
|
+
destination branch, and proves content by full-tree equality or an
|
|
113
|
+
exact path-by-path transition (paths, modes, symlinks, binary
|
|
114
|
+
content) between separate bases. Never fetch proof refs into shared
|
|
115
|
+
repositories or worktrees, and never interpret diff-tool
|
|
116
|
+
output, commit titles, PR reports, or CI status as content proof.
|
|
117
|
+
3. Patch-range proof additionally requires the independently recorded
|
|
118
|
+
attempt baseline (`base_head`), and that baseline itself must be
|
|
119
|
+
preserved on a surviving accepted ref; without it only full-tree
|
|
120
|
+
equality or accepted ancestry proves preservation. Empty or
|
|
121
|
+
caller-truncated ranges are not evidence.
|
|
141
122
|
4. Ambiguity preserves: conflicting hashes, an unverifiable destination,
|
|
142
123
|
or a failed equivalence check means no proof -- block the prune per
|
|
143
124
|
step 5.
|
|
@@ -151,9 +132,13 @@ candidate:
|
|
|
151
132
|
|
|
152
133
|
For assignment-backed candidates also check `ace-assign status`; for Lab
|
|
153
134
|
runtime candidates check `ace-overseer status --runtime lab`. A running
|
|
154
|
-
assignment, an in-flight Lab work, or any
|
|
155
|
-
prune. Missing or unreadable lifecycle
|
|
156
|
-
-- preserve.
|
|
135
|
+
assignment, an active or uncertain attempt, an in-flight Lab work, or any
|
|
136
|
+
unaccounted writer blocks the prune. Missing or unreadable lifecycle
|
|
137
|
+
state counts as an active writer -- preserve. `ace-overseer prune`
|
|
138
|
+
enforces this itself: apply holds a durable exclusion shared with every
|
|
139
|
+
supported start path from final evidence reads through removal, so a
|
|
140
|
+
writer cannot start into a candidate being deleted, and a candidate that
|
|
141
|
+
changes after preview is re-blocked at the destructive boundary.
|
|
157
142
|
|
|
158
143
|
4. A described or remembered proof is never sufficient -- run the commands
|
|
159
144
|
and observe the results in this session.
|
|
@@ -161,18 +146,18 @@ candidate:
|
|
|
161
146
|
5. Any commit without a proven copy, any failed or ambiguous equivalence,
|
|
162
147
|
or any active-writer evidence **blocks the prune** for that worktree.
|
|
163
148
|
Report the blocked candidate (ref, head SHA, missing proof) to the
|
|
164
|
-
operator; never silently drop it and never
|
|
165
|
-
Preserve on ambiguity.
|
|
149
|
+
operator; never silently drop it and never bypass a failed proof with
|
|
150
|
+
`--force`. Preserve on ambiguity.
|
|
166
151
|
|
|
167
152
|
## Non-Negotiable Contracts Summary
|
|
168
153
|
|
|
169
154
|
| Contract | Claim | Executed proof |
|
|
170
155
|
|--------------|-------|----------------|
|
|
171
|
-
| Prune safety | "The work is preserved and nothing is actively writing" | `git merge-base --is-ancestor <work-head> <base>` (rc=0), or verified destination
|
|
156
|
+
| Prune safety | "The work is preserved and nothing is actively writing" | `git merge-base --is-ancestor <work-head> <base>` (rc=0), or a verified declared destination (--preservation) with tree/artifact or exact content-transition equivalence; plus the executed no-writer reconciliation (overseer/assignment/Lab status, terminal attempts, durable prune/start exclusion) |
|
|
172
157
|
| Status truth | "The task is blocked on the owner" | An executable non-secret check run in this session; stale tasks closed/corrected in the same change |
|
|
173
158
|
|
|
174
159
|
## Success Criteria
|
|
175
160
|
|
|
176
161
|
- Work starts only through `ace-overseer work-on` (or the Lab-runtime equivalent), never through ad-hoc manual worktree provisioning.
|
|
177
162
|
- Every status review re-verified pending owner-blocked tasks with an executed check and corrected stale state in the same change.
|
|
178
|
-
- Every pruned worktree/branch had an executed preservation proof (ancestor containment, or a verified destination with tree/artifact or
|
|
163
|
+
- Every pruned worktree/branch had an executed preservation proof (ancestor containment, or a verified destination with tree/artifact or exact content-transition equivalence) and an executed no-active-writer check; candidates without complete proof were reported as blocked, not removed; `--force` and `--yes` never bypassed a safety block.
|
|
@@ -7,24 +7,31 @@ module Ace
|
|
|
7
7
|
class Prune < Ace::Support::Cli::Command
|
|
8
8
|
include Ace::Support::Cli::Base
|
|
9
9
|
|
|
10
|
-
desc "Clean up completed task worktrees"
|
|
10
|
+
desc "Clean up completed task worktrees with executed preservation and no-writer proofs"
|
|
11
11
|
|
|
12
12
|
argument :targets, required: false, type: :array, desc: "Task refs or folder names to prune"
|
|
13
13
|
|
|
14
14
|
option :assignment, aliases: ["-a"], type: :string, desc: "Prune a specific assignment by ID"
|
|
15
|
-
option :force, aliases: ["-f"], type: :boolean, default: false,
|
|
15
|
+
option :force, aliases: ["-f"], type: :boolean, default: false,
|
|
16
|
+
desc: "Skip the confirmation prompt for already-safe candidates; never bypasses safety blocks"
|
|
16
17
|
option :yes, aliases: ["-y"], type: :boolean, default: false, desc: "Skip confirmation"
|
|
17
|
-
option :dry_run, type: :boolean, default: false, desc: "Show candidates only"
|
|
18
|
-
option :quiet, aliases: ["-q"], type: :boolean, default: false,
|
|
18
|
+
option :dry_run, type: :boolean, default: false, desc: "Show candidates only (no cleanup side effects)"
|
|
19
|
+
option :quiet, aliases: ["-q"], type: :boolean, default: false,
|
|
20
|
+
desc: "Suppress progress output; failures still print and signal via exit code"
|
|
19
21
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
20
22
|
option :runtime, default: "tmux", desc: "Runtime (tmux, lab)"
|
|
23
|
+
option :preservation, type: :string,
|
|
24
|
+
desc: "YAML manifest (version 1) declaring verified cross-repository destinations"
|
|
21
25
|
|
|
22
|
-
def initialize(orchestrator: nil, input: $stdin, output: $stdout, lab_client: nil
|
|
26
|
+
def initialize(orchestrator: nil, input: $stdin, output: $stdout, lab_client: nil,
|
|
27
|
+
lab_safety_checker: nil, preservation_checker: nil)
|
|
23
28
|
super()
|
|
24
29
|
@orchestrator = orchestrator || Organisms::PruneOrchestrator.new
|
|
25
30
|
@input = input
|
|
26
31
|
@output = output
|
|
27
32
|
@lab_client = lab_client || Molecules::LabClient.new
|
|
33
|
+
@lab_safety_checker = lab_safety_checker || Molecules::LabPruneSafetyChecker.new
|
|
34
|
+
@preservation_checker = preservation_checker || Molecules::GitPreservationChecker.new
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def call(**options)
|
|
@@ -45,15 +52,22 @@ module Ace
|
|
|
45
52
|
force: options[:force],
|
|
46
53
|
targets: targets,
|
|
47
54
|
assignment_id: options[:assignment],
|
|
55
|
+
preservation_manifest: options[:preservation],
|
|
48
56
|
input: @input,
|
|
49
57
|
output: @output,
|
|
50
58
|
on_progress: progress
|
|
51
59
|
)
|
|
52
60
|
|
|
53
|
-
return if options[:quiet]
|
|
54
|
-
|
|
55
61
|
if options[:assignment]
|
|
56
62
|
print_assignment_result(result)
|
|
63
|
+
return if options[:dry_run] || result[:aborted]
|
|
64
|
+
|
|
65
|
+
raise Ace::Support::Cli::Error, "Assignment #{options[:assignment]} is blocked: " \
|
|
66
|
+
"#{result[:assignment_candidate].reasons.join(", ")}" if result[:blocked]
|
|
67
|
+
unless result[:pruned_assignments].any?
|
|
68
|
+
raise Ace::Support::Cli::Error, "Failed to remove assignment #{options[:assignment]}"
|
|
69
|
+
end
|
|
70
|
+
|
|
57
71
|
return
|
|
58
72
|
end
|
|
59
73
|
|
|
@@ -62,14 +76,19 @@ module Ace
|
|
|
62
76
|
return
|
|
63
77
|
end
|
|
64
78
|
|
|
65
|
-
if result[:aborted]
|
|
66
|
-
|
|
67
|
-
return
|
|
68
|
-
end
|
|
79
|
+
puts "Prune aborted." if result[:aborted]
|
|
80
|
+
return if result[:aborted]
|
|
69
81
|
|
|
70
82
|
print_apply(result)
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
|
|
84
|
+
blocked_count = Array(result[:unsafe]).length + Array(result[:blocked]).length +
|
|
85
|
+
Array(result[:failed]).length
|
|
86
|
+
return if blocked_count.zero?
|
|
87
|
+
|
|
88
|
+
raise Ace::Support::Cli::Error,
|
|
89
|
+
"#{blocked_count} candidate(s) blocked or failed; nothing unsafe was removed"
|
|
90
|
+
rescue Ace::Overseer::Error => e
|
|
91
|
+
raise Ace::Support::Cli::Error, e.message
|
|
73
92
|
end
|
|
74
93
|
|
|
75
94
|
private
|
|
@@ -77,23 +96,112 @@ module Ace
|
|
|
77
96
|
def prune_lab(**options)
|
|
78
97
|
raise Ace::Support::Cli::Error, "--assignment is not supported with Lab runtime; provide exact Work IDs" if options[:assignment]
|
|
79
98
|
raise Ace::Support::Cli::Error, "--force is not supported with Lab runtime" if options[:force]
|
|
99
|
+
raise Ace::Support::Cli::Error, "--preservation is not supported with Lab runtime" if options[:preservation]
|
|
80
100
|
|
|
81
101
|
works = Array(options[:targets]).map(&:to_s)
|
|
82
102
|
raise Ace::Support::Cli::Error, "provide at least one exact Lab Work ID to prune" if works.empty?
|
|
83
103
|
|
|
84
|
-
if
|
|
85
|
-
|
|
104
|
+
classifications = if atomic_destroy_supported?
|
|
105
|
+
works.to_h do |work|
|
|
106
|
+
[work, @lab_safety_checker.check(
|
|
107
|
+
lab_client: @lab_client,
|
|
108
|
+
work_id: work,
|
|
109
|
+
preservation_proof: lab_preservation_proof(work)
|
|
110
|
+
)]
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
# The raw Lab surface cannot make the no-writer check and the
|
|
114
|
+
# destruction atomic; claiming a probe was safe would be a
|
|
115
|
+
# lie, so the whole path is unsupported and preserved.
|
|
116
|
+
unsupported = Molecules::LabPruneSafetyChecker::Classification.new(
|
|
117
|
+
safe?: false,
|
|
118
|
+
reason: "lab runtime cannot make the no-writer check and destruction atomic; " \
|
|
119
|
+
"preserve the Work instead"
|
|
120
|
+
)
|
|
121
|
+
works.to_h { |work| [work, unsupported] }
|
|
122
|
+
end
|
|
86
123
|
|
|
87
|
-
|
|
124
|
+
if options[:dry_run]
|
|
125
|
+
print_lab_dry_run(classifications)
|
|
88
126
|
return
|
|
89
127
|
end
|
|
90
128
|
|
|
91
129
|
raise Ace::Support::Cli::Error, "Lab prune requires --yes after reviewing --dry-run" unless options[:yes]
|
|
92
130
|
|
|
131
|
+
blocked = []
|
|
132
|
+
destroyed = []
|
|
93
133
|
works.each do |work|
|
|
134
|
+
classification = classifications[work]
|
|
135
|
+
unless classification.safe?
|
|
136
|
+
blocked << [work, classification.reason]
|
|
137
|
+
next
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Guarded delegation: re-read the authoritative state
|
|
141
|
+
# immediately before destroy; a changed state aborts.
|
|
142
|
+
recheck = @lab_safety_checker.check(
|
|
143
|
+
lab_client: @lab_client,
|
|
144
|
+
work_id: work,
|
|
145
|
+
preservation_proof: lab_preservation_proof(work)
|
|
146
|
+
)
|
|
147
|
+
unless recheck.safe?
|
|
148
|
+
blocked << [work, "state changed before destroy: #{recheck.reason}"]
|
|
149
|
+
next
|
|
150
|
+
end
|
|
151
|
+
|
|
94
152
|
result = @lab_client.call("work", "destroy", work, "--confirm", json: false)
|
|
153
|
+
destroyed << work
|
|
95
154
|
puts result unless options[:quiet]
|
|
96
155
|
end
|
|
156
|
+
|
|
157
|
+
blocked.each do |work, reason|
|
|
158
|
+
puts "Blocked: lab work #{work}: #{reason}"
|
|
159
|
+
end
|
|
160
|
+
puts "#{destroyed.length} lab work(s) destroyed." unless options[:quiet]
|
|
161
|
+
return if blocked.empty?
|
|
162
|
+
|
|
163
|
+
raise Ace::Support::Cli::Error, "#{blocked.length} lab work(s) blocked; nothing unsafe was destroyed"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Delegation is only honest when the Lab surface itself can make
|
|
167
|
+
# the state check and the destruction atomic. The raw CLI adapter
|
|
168
|
+
# cannot, so it reports unsupported; adapters with an atomic
|
|
169
|
+
# guarded destroy opt in.
|
|
170
|
+
def atomic_destroy_supported?
|
|
171
|
+
@lab_client.respond_to?(:supports_atomic_destroy?) && @lab_client.supports_atomic_destroy?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Preservation evidence for a Lab Work comes from the Work's own
|
|
175
|
+
# documented surviving identity (repo/head/branch). A surface that
|
|
176
|
+
# does not document it cannot prove preservation.
|
|
177
|
+
def lab_preservation_proof(work)
|
|
178
|
+
entry = @lab_client.work_entry(work)
|
|
179
|
+
return Models::PreservationProof.blocked("lab work #{work} not found in authoritative status") if entry.nil?
|
|
180
|
+
|
|
181
|
+
repo = entry["repo"].to_s
|
|
182
|
+
head = entry["head"].to_s
|
|
183
|
+
branch = entry["branch"].to_s
|
|
184
|
+
if repo.empty? || head.empty? || branch.empty? || !repo.start_with?("/")
|
|
185
|
+
return Models::PreservationProof.blocked(
|
|
186
|
+
"lab work #{work} does not document preservation data (repo/head/branch)"
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
accepted_base = @preservation_checker.accepted_base_for(repo)
|
|
191
|
+
@preservation_checker.ancestry_proof(repo: repo, head: head, accepted_base: accepted_base)
|
|
192
|
+
rescue Ace::Overseer::Error => e
|
|
193
|
+
Models::PreservationProof.blocked("lab preservation evidence unavailable: #{e.message}")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def print_lab_dry_run(classifications)
|
|
197
|
+
puts "Lab prune classification:"
|
|
198
|
+
classifications.each do |work, classification|
|
|
199
|
+
if classification.safe?
|
|
200
|
+
puts " #{work}: safe (terminal, preserved, no in-flight work)"
|
|
201
|
+
else
|
|
202
|
+
puts " #{work}: BLOCKED — #{classification.reason}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
97
205
|
end
|
|
98
206
|
|
|
99
207
|
def print_assignment_result(result)
|
|
@@ -112,6 +220,7 @@ module Ace
|
|
|
112
220
|
end
|
|
113
221
|
|
|
114
222
|
if result[:blocked]
|
|
223
|
+
puts "Blocked: assignment #{candidate.assignment_id}: #{candidate.reasons.join(", ")}"
|
|
115
224
|
return
|
|
116
225
|
end
|
|
117
226
|
|
|
@@ -124,27 +233,36 @@ module Ace
|
|
|
124
233
|
end
|
|
125
234
|
|
|
126
235
|
def print_dry_run(result)
|
|
127
|
-
forced = Array(result[:forced])
|
|
128
236
|
puts "Candidates for cleanup:"
|
|
129
|
-
if result[:safe].empty?
|
|
237
|
+
if result[:safe].empty?
|
|
130
238
|
puts " (none)"
|
|
131
239
|
else
|
|
132
240
|
result[:safe].each do |candidate|
|
|
133
241
|
puts " task.#{candidate.task_id} - #{candidate.worktree_path}"
|
|
134
242
|
end
|
|
135
|
-
|
|
136
|
-
|
|
243
|
+
end
|
|
244
|
+
if result[:unsafe].any?
|
|
245
|
+
puts "Blocked candidates:"
|
|
246
|
+
result[:unsafe].each do |candidate|
|
|
247
|
+
puts " task.#{candidate.task_id} - #{candidate.worktree_path}"
|
|
248
|
+
candidate.reasons.each { |reason| puts " - #{reason}" }
|
|
137
249
|
end
|
|
138
250
|
end
|
|
139
251
|
puts
|
|
140
|
-
|
|
141
|
-
|
|
252
|
+
puts "#{result[:safe].length} worktree(s) can be pruned; " \
|
|
253
|
+
"#{result[:unsafe].length} blocked."
|
|
142
254
|
end
|
|
143
255
|
|
|
144
256
|
def print_apply(result)
|
|
145
257
|
result[:pruned].each do |candidate|
|
|
146
258
|
puts "Removed worktree task.#{candidate.task_id}"
|
|
147
259
|
end
|
|
260
|
+
Array(result[:unsafe]).each do |candidate|
|
|
261
|
+
puts "Blocked: task.#{candidate.task_id}: #{candidate.reasons.join(", ")}"
|
|
262
|
+
end
|
|
263
|
+
Array(result[:blocked]).each do |entry|
|
|
264
|
+
puts "Blocked after recheck: task.#{entry[:candidate].task_id}: #{entry[:reasons].join(", ")}"
|
|
265
|
+
end
|
|
148
266
|
result[:failed].each do |entry|
|
|
149
267
|
puts "Failed to remove task.#{entry[:candidate].task_id}: #{entry[:error]}"
|
|
150
268
|
end
|
|
@@ -4,18 +4,21 @@ module Ace
|
|
|
4
4
|
module Overseer
|
|
5
5
|
module Models
|
|
6
6
|
class AssignmentPruneCandidate
|
|
7
|
-
attr_reader :assignment_id, :assignment_name, :assignment_state, :location_path,
|
|
7
|
+
attr_reader :assignment_id, :assignment_name, :assignment_state, :location_path,
|
|
8
|
+
:attempts_terminal, :reasons
|
|
8
9
|
|
|
9
|
-
def initialize(assignment_id:, assignment_name:, assignment_state:, location_path:,
|
|
10
|
+
def initialize(assignment_id:, assignment_name:, assignment_state:, location_path:,
|
|
11
|
+
attempts_terminal: true, reasons: [])
|
|
10
12
|
@assignment_id = assignment_id.to_s.freeze
|
|
11
13
|
@assignment_name = assignment_name.to_s.freeze
|
|
12
14
|
@assignment_state = assignment_state.to_s.freeze
|
|
13
15
|
@location_path = location_path.to_s.freeze
|
|
16
|
+
@attempts_terminal = attempts_terminal
|
|
14
17
|
@reasons = reasons.map(&:to_s).freeze
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def safe_to_prune?
|
|
18
|
-
assignment_state == "completed"
|
|
21
|
+
assignment_state == "completed" && attempts_terminal
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def to_h
|
|
@@ -24,6 +27,7 @@ module Ace
|
|
|
24
27
|
assignment_name: assignment_name,
|
|
25
28
|
assignment_state: assignment_state,
|
|
26
29
|
location_path: location_path,
|
|
30
|
+
attempts_terminal: attempts_terminal,
|
|
27
31
|
reasons: reasons,
|
|
28
32
|
safe_to_prune: safe_to_prune?
|
|
29
33
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Overseer
|
|
5
|
+
module Models
|
|
6
|
+
# Outcome of a preservation proof for one prune candidate.
|
|
7
|
+
#
|
|
8
|
+
# A proof is preserved only through an executed Git identity: accepted
|
|
9
|
+
# ancestor containment, full-tree equality, or an exact content
|
|
10
|
+
# transition against a verified destination. Anything else — missing,
|
|
11
|
+
# failed or ambiguous evidence — is not preserved and carries the
|
|
12
|
+
# blocking reason.
|
|
13
|
+
class PreservationProof
|
|
14
|
+
attr_reader :method, :reason, :head
|
|
15
|
+
|
|
16
|
+
def initialize(preserved:, method: nil, reason: nil, head: nil)
|
|
17
|
+
@preserved = preserved
|
|
18
|
+
@method = method
|
|
19
|
+
@reason = reason
|
|
20
|
+
@head = head
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.preserved(method, head: nil)
|
|
24
|
+
new(preserved: true, method: method, head: head)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.blocked(reason)
|
|
28
|
+
new(preserved: false, reason: reason)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def preserved?
|
|
32
|
+
@preserved
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|