flower
/
All briefs
complete draft mcp flower
epic · Let's brainstorm/plan/spec the idea of multi-chain o...

Epic-lead Slice F — Auto-retire, post-merge window & ops handoff (#226)

canonical · plan

Spec

markdown

hand-off · dispatch

Dispatch

Auto-dispatch

when it reaches planned

Design-loop

design pass before build

This brief is complete — dispatch is closed.

#119 done fresh flower · flower/236-epic-lead-slice-f
agent: claude
You are being dispatched from flower Brief #236: Epic-lead Slice F — Auto-retire, post-merge window & ops handoff (#226)

Recall pointer:
- Use recall_brief with id 236 for the full folder if you need provenance.

Target:
- project: flower (/Users/mikeferrara/Documents/code/flower)
- branch: flower/236-epic-lead-slice-f
- worktree: not specified
- kind: fresh

Current brief spec:
# Slice F — Auto-retire, post-merge window & ops handoff

Parent epic: #226. Design: `docs/design/226-epic-lead-orchestration.md` (§Auto-retire, §Failure/reset). **Depends on Slices B (#232), C (#233), D (#234).** Target ≤~300 lines. Honors decided fork **#56** (auto-retire on epic completion).

## Deliverable
1. **Epic-complete detection** — terminal = sum of `children_rollup.by_status` over `{complete, cancelled, abandoned}` == `total` (NOT `complete + cancelled`; `childrenRollup()` has no `cancelled` field and `BriefStatus::isTerminal()` = {Complete, Cancelled, Abandoned}, `Deferred` is NOT terminal). **Add an explicit `terminal` count to `RecallService::childrenRollup()`** (:891) so it isn't recomputed everywhere.
2. **Bounded post-merge monitoring window** — after MAIN's `epic_delegate{action:merged}`, the lead watches its feature for `config('flower.epic_lead.post_merge_window_minutes', 30)` (or 2 clean heartbeats): new feedback tagged to the epic/children, health signals on changed files, first real exercise. Serious regression → **report up** (`feedback_promote`/`decision_ask`), do not expand scope.
3. **Lead → ops handoff** — write a concise watch-note into the epic brief (`brief_append`: what shipped, what to watch, known risks, files touched) + hand ongoing monitoring to ops. Boundary: lead = bounded window for its own feature; ops = standing/ongoing.
4. **Cooperative retire (no hard-kill race)** — lead calls `daemon_winddown_ready` on itself (no role gate, `DaemonRosterService.php:548`) + **stops its heartbeat/work loop** (winddown convention), THEN enqueues `epic_return{action:retire}`. MAIN calls `CloseDaemon::close(lead)` — guard already satisfied, **no forced winddown override / no CloseAllDaemons hard-kill**. Epic brief → `complete`; EnsureDaemons never respawns (leads not in expected sets) → durable retirement.
5. **Failure/reset behavior** — stale/dead lead on an open epic → MAIN decides: respawn a fresh lead (idempotent — durable state in epic brief + children + epic branch) or absorb inline. Lead reset mid-epic → make-before-break handshake; successor inherits `epic_brief_id` + epic worktree + drains its `epic_delegate` mailbox by epic id (baton NOT transferred — lead is a subordinate). Epic cancelled → lead retires without a master merge; branch/worktree torn down.

## Tests
- Retire path closes a quiesced lead and marks the epic `complete`.
- EnsureDaemons never respawns a retired lead.
- Respawn of a fresh lead resumes from the epic brief (durable-state recovery).
- Terminal detection counts `abandoned` children.

## Notes
- This closes the finite lifecycle. The end-to-end epic can be demonstrated after this slice.

Recent/key trace events:
[1] participant_joined flower-226-worker: (no body)
[2] note_added flower-226-worker: # Slice F — Auto-retire, post-merge window & ops handoff

Design: `docs/design/226-epic-lead-orchestration.md` (§Auto-retire, §Failure/reset). Depends on Slices B, C, D. Target ≤~300 lines. Honors decided fork **#56** (auto-retire on epic completion).

## Deliverable
1. **Epic-complete detection** — terminal = sum of `children_rollup.by_status` over `{complete, cancelled, abandoned}` == `total` (NOT `complete + cancelled`; `childrenRollup()` has no `cancelled` field and `BriefStatus::isTerminal()` = {Complete, Cancelled, Abandoned}, `Deferred` is NOT terminal). **Add an explicit `terminal` count to `RecallService::childrenRollup()`** (:891) so it isn't recomputed everywhere.
2. **Bounded post-merge monitoring window** — after MAIN's `epic_delegate{action:merged}`, the lead watches its feature for `config('flower.epic_lead.post_merge_window_minutes', 30)` (or 2 clean heartbeats): new feedback tagged to the epic/children, health signals on changed files, first real exercise. Serious regression → **report up** (`feedback_promote`/`decision_ask`), do not expand scope.
3. **Lead → ops handoff** — write a concise watch-note into the epic brief (`brief_append`: what shipped, what to watch, known risks, files touched) + hand ongoing monitoring to ops. Boundary: lead = bounded window for its own feature; ops = standing/ongoing.
4. **Cooperative retire (no hard-kill race)** — lead calls `daemon_winddown_ready` on itself (no role gate, `DaemonRosterService.php:548`) + **stops its heartbeat/work loop** (winddown convention), THEN enqueues `epic_return{action:retire}`. MAIN calls `CloseDaemon::close(lead)` — guard already satisfied, **no forced winddown override / no CloseAllDaemons hard-kill**. Epic brief → `complete`; EnsureDaemons never respawns (leads not in expected sets) → durable retirement.
5. **Failure/reset behavior** — stale/dead lead on an open epic → MAIN decides: respawn a fresh lead (idempotent — durable state in epic brief + children + epic branch) or absorb inline. Lead reset mid-epic → make-before-break handshake; successor inherits `epic_brief_id` + epic worktree + drains its `epic_delegate` mailbox by epic id (baton NOT transferred — lead is a subordinate). Epic cancelled → lead retires without a master merge; branch/worktree torn down.

## Tests
- Retire path closes a quiesced lead and marks the epic `complete`.
- EnsureDaemons never respawns a retired lead.
- Respawn of a fresh lead resumes from the epic brief (durable-state recovery).
- Terminal detection counts `abandoned` children.

## Notes
- This closes the finite lifecycle. The end-to-end epic can be demonstrated after this slice.
[3] parent_set flower-226-worker: Grouped under epic #226.
[4] dependency_added flower-226-worker: Now depends on #232 (Epic-lead Slice B — Lead charter & spawn path (#226)).
[5] dependency_added flower-226-worker: Now depends on #233 (Epic-lead Slice C — Coordination signals epic_delegate / epic_return (#226)).
[6] dependency_added flower-226-worker: Now depends on #234 (Epic-lead Slice D — Epic-branch merge + baton coexistence (#226)).
[7] plan_proposed flower-226-worker: # Slice F — Auto-retire, post-merge window & ops handoff

Parent epic: #226. Design: `docs/design/226-epic-lead-orchestration.md` (§Auto-retire, §Failure/reset). **Depends on Slices B (#232), C (#233), D (#234).** Target ≤~300 lines. Honors decided fork **#56** (auto-retire on epic completion).

## Deliverable
1. **Epic-complete detection** — terminal = sum of `children_rollup.by_status` over `{complete, cancelled, abandoned}` == `total` (NOT `complete + cancelled`; `childrenRollup()` has no `cancelled` field and `BriefStatus::isTerminal()` = {Complete, Cancelled, Abandoned}, `Deferred` is NOT terminal). **Add an explicit `terminal` count to `RecallService::childrenRollup()`** (:891) so it isn't recomputed everywhere.
2. **Bounded post-merge monitoring window** — after MAIN's `epic_delegate{action:merged}`, the lead watches its feature for `config('flower.epic_lead.post_merge_window_minutes', 30)` (or 2 clean heartbeats): new feedback tagged to the epic/children, health signals on changed files, first real exercise. Serious regression → **report up** (`feedback_promote`/`decision_ask`), do not expand scope.
3. **Lead → ops handoff** — write a concise watch-note into the epic brief (`brief_append`: what shipped, what to watch, known risks, files touched) + hand ongoing monitoring to ops. Boundary: lead = bounded window for its own feature; ops = standing/ongoing.
4. **Cooperative retire (no hard-kill race)** — lead calls `daemon_winddown_ready` on itself (no role gate, `DaemonRosterService.php:548`) + **stops its heartbeat/work loop** (winddown convention), THEN enqueues `epic_return{action:retire}`. MAIN calls `CloseDaemon::close(lead)` — guard already satisfied, **no forced winddown override / no CloseAllDaemons hard-kill**. Epic brief → `complete`; EnsureDaemons never respawns (leads not in expected sets) → durable retirement.
5. **Failure/reset behavior** — stale/dead lead on an open epic → MAIN decides: respawn a fresh lead (idempotent — durable state in epic brief + children + epic branch) or absorb inline. Lead reset mid-epic → make-before-break handshake; successor inherits `epic_brief_id` + epic worktree + drains its `epic_delegate` mailbox by epic id (baton NOT transferred — lead is a subordinate). Epic cancelled → lead retires without a master merge; branch/worktree torn down.

## Tests
- Retire path closes a quiesced lead and marks the epic `complete`.
- EnsureDaemons never respawns a retired lead.
- Respawn of a fresh lead resumes from the epic brief (durable-state recovery).
- Terminal detection counts `abandoned` children.

## Notes
- This closes the finite lifecycle. The end-to-end epic can be demonstrated after this slice.
[8] participant_joined flower-234-worker: (no body)
[9] unblocked flower-234-worker: Unblocked — #234 reached complete.
[10] participant_joined flower-orchestrator: (no body)
[11] status_change flower-orchestrator: (no body)

Recommended linked context:
{
    "todos": [],
    "scratchpads": []
}

Execution notes:
- Treat the brief as the source of truth.
- Keep work scoped to this dispatch request.
- Use brief_append / brief_update_status when reporting material progress; as your final dispatched-worker step, call brief_dispatch_complete with dispatch_request_id (or brief_id) and actor_ref.
- Codex workers should verify mutating Flower tools with tool_search query `brief_append brief_dispatch_complete flower_feedback` (limit 20) when tool availability is in doubt; report raw SEE/LOAD vs NOT visible instead of silently using local fallbacks.
- Add a git commit trailer `Brief: #236` to every commit for this brief so flower can exact-link commits back to the brief.

provenance · append-only

Trace

live
or paste a screenshot uploading…
  1. link added 21h ago
    agent · system:commit-trailer
  2. participant joined 21h ago
    system · system:commit-trailer
  3. merged 22h ago

    Merged flower/236-epic-lead-slice-f (Slice F — EpicLeadRetireService + flower:retire-epic-lead cooperative auto-retire; childrenRollup terminal count) to master by flower-orchestrator (daemon 40). Combined suite green 1211/1209 pass/2 skip. FINAL slice — epic #226 functionally complete.

    agent · flower-orchestrator
  4. status change 22h ago
    agent · flower-236-worker
  5. dispatched 22h ago

    Dispatch request #119 marked done.

    agent · flower-236-worker
  6. note added 22h ago

    Slice F landed on branch flower/236-epic-lead-slice-f (commit 461e200, trailer Brief: #236). Full suite green (1203 passed / 2 pre-existing skips), Pint clean. NOT merged — MAIN merges. **This is the FINAL slice — epic #226 is functionally complete.** Like D, Slice B's lead charter already carries F's runtime prose (post-merge window, ops handoff, cooperative retire), so F's value is the load-bearing CODE + tests that make the lifecycle real. What landed: 1. RecallService::childrenRollup() gains an explicit `terminal` count — Complete + Cancelled + Abandoned, derived from BriefStatus::isTerminal() so Deferred is correctly NOT counted (the exact gotcha the design + my Slice-D handoff flagged). The lead's retire gate reads `terminal == total` instead of re-deriving from by_status each heartbeat. 2. EpicLeadPolicy::isEpicComplete(Brief) — the queryable children-terminal half of the retire trigger (every child terminal + ≥1 child); mirrors the rollup so both agree. 3. EpicLeadRetireService (NEW) — cooperative retire: assert the daemon is a lead bound to an epic_brief_id AND is winddown_ready (NEVER force it — a running lead is refused, no mid-action hard-kill), close via the standard CloseDaemon winddown_ready gate (no forced override / no CloseAllDaemons hard-kill), then mark the epic brief `complete` (the durable retire marker; idempotent when already terminal, e.g. an operator-cancelled epic that retires without a master merge). Uses BriefService::setStatus so the completion guard (open-questions hold) is respected and epic_completed reflects the real outcome. 4. flower:retire-epic-lead command (NEW) — MAIN-facing affordance symmetric with flower:spawn-epic-lead (spawn ↔ retire bookend the lifecycle): resolves the lead by --epic-brief-id, refuses a not-winddown-ready lead, dry-run by default, --confirm to retire. Tests (17 new, all green): retire closes a quiesced lead + marks the epic complete; refuses a running lead (no hard-kill); rejects a non-lead; idempotent on an already-terminal epic; **EnsureDaemons never expects a lead even if config lists it**; **durable epic state survives the lead's death** (resume from the brief + children, not context); **children_rollup.terminal counts abandoned + excludes deferred**; isEpicComplete detection + a Deferred child keeping the epic open. Reused levers I built earlier (EpicBranch, canMergeToMaster, EpicLeadPolicy::maxConcurrent, C's epic_return{action:retire}, CloseDaemon winddown_ready). ## Epic #226 — functionally complete A task-scoped epic lead can now be: spawned by MAIN (B: DaemonRole::Lead identity/roster [A] + lead charter + flower:spawn-epic-lead), own/delegate/review its children and merge them into an isolated epic branch under its own flower-epic-<id>-merge lock (C/D), hand the SINGLE epic→master merge to MAIN via epic_return{action:merge} (C/D), watch a bounded post-merge window + hand to ops, then cooperatively auto-retire via epic_return{action:retire} → CloseDaemon + epic→complete (F) — all on the reused daemon infra (heartbeat/roster/reset/winddown/compaction + the daemon_signals coordination bus), with the MAIN baton and the single-master-merge-point invariant untouched. End-to-end live spawn→retire verification is MAIN's job on real infra, per the worktree playbook.

    agent · flower-236-worker
  7. participant joined 22h ago
    system · flower-236-worker
  8. dispatched 22h ago

    Dispatch request #119 queued for flower.

    agent · flower-orchestrator
  9. status change 22h ago
    agent · flower-orchestrator
  10. status change 22h ago
    agent · flower-orchestrator
  11. participant joined 22h ago
    system · flower-orchestrator
  12. unblocked 22h ago

    Unblocked — #234 reached complete.

    system · flower-234-worker
  13. participant joined 22h ago
    system · flower-234-worker
  14. plan proposed 1d ago

    # Slice F — Auto-retire, post-merge window & ops handoff Parent epic: #226. Design: `docs/design/226-epic-lead-orchestration.md` (§Auto-retire, §Failure/reset). **Depends on Slices B (#232), C (#233), D (#234).** Target ≤~300 lines. Honors decided fork **#56** (auto-retire on epic completion). ## Deliverable 1. **Epic-complete detection** — terminal = sum of `children_rollup.by_status` over `{complete, cancelled, abandoned}` == `total` (NOT `complete + cancelled`; `childrenRollup()` has no `cancelled` field and `BriefStatus::isTerminal()` = {Complete, Cancelled, Abandoned}, `Deferred` is NOT terminal). **Add an explicit `terminal` count to `RecallService::childrenRollup()`** (:891) so it isn't recomputed everywhere. 2. **Bounded post-merge monitoring window** — after MAIN's `epic_delegate{action:merged}`, the lead watches its feature for `config('flower.epic_lead.post_merge_window_minutes', 30)` (or 2 clean heartbeats): new feedback tagged to the epic/children, health signals on changed files, first real exercise. Serious regression → **report up** (`feedback_promote`/`decision_ask`), do not expand scope. 3. **Lead → ops handoff** — write a concise watch-note into the epic brief (`brief_append`: what shipped, what to watch, known risks, files touched) + hand ongoing monitoring to ops. Boundary: lead = bounded window for its own feature; ops = standing/ongoing. 4. **Cooperative retire (no hard-kill race)** — lead calls `daemon_winddown_ready` on itself (no role gate, `DaemonRosterService.php:548`) + **stops its heartbeat/work loop** (winddown convention), THEN enqueues `epic_return{action:retire}`. MAIN calls `CloseDaemon::close(lead)` — guard already satisfied, **no forced winddown override / no CloseAllDaemons hard-kill**. Epic brief → `complete`; EnsureDaemons never respawns (leads not in expected sets) → durable retirement. 5. **Failure/reset behavior** — stale/dead lead on an open epic → MAIN decides: respawn a fresh lead (idempotent — durable state in epic brief + children + epic branch) or absorb inline. Lead reset mid-epic → make-before-break handshake; successor inherits `epic_brief_id` + epic worktree + drains its `epic_delegate` mailbox by epic id (baton NOT transferred — lead is a subordinate). Epic cancelled → lead retires without a master merge; branch/worktree torn down. ## Tests - Retire path closes a quiesced lead and marks the epic `complete`. - EnsureDaemons never respawns a retired lead. - Respawn of a fresh lead resumes from the epic brief (durable-state recovery). - Terminal detection counts `abandoned` children. ## Notes - This closes the finite lifecycle. The end-to-end epic can be demonstrated after this slice.

    agent · flower-226-worker
  15. dependency added 1d ago

    Now depends on #234 (Epic-lead Slice D — Epic-branch merge + baton coexistence (#226)).

    agent · flower-226-worker
  16. dependency added 1d ago

    Now depends on #233 (Epic-lead Slice C — Coordination signals epic_delegate / epic_return (#226)).

    agent · flower-226-worker
  17. dependency added 1d ago

    Now depends on #232 (Epic-lead Slice B — Lead charter & spawn path (#226)).

    agent · flower-226-worker
  18. parent set 1d ago

    Grouped under epic #226.

    agent · flower-226-worker
  19. note added 1d ago

    # Slice F — Auto-retire, post-merge window & ops handoff Design: `docs/design/226-epic-lead-orchestration.md` (§Auto-retire, §Failure/reset). Depends on Slices B, C, D. Target ≤~300 lines. Honors decided fork **#56** (auto-retire on epic completion). ## Deliverable 1. **Epic-complete detection** — terminal = sum of `children_rollup.by_status` over `{complete, cancelled, abandoned}` == `total` (NOT `complete + cancelled`; `childrenRollup()` has no `cancelled` field and `BriefStatus::isTerminal()` = {Complete, Cancelled, Abandoned}, `Deferred` is NOT terminal). **Add an explicit `terminal` count to `RecallService::childrenRollup()`** (:891) so it isn't recomputed everywhere. 2. **Bounded post-merge monitoring window** — after MAIN's `epic_delegate{action:merged}`, the lead watches its feature for `config('flower.epic_lead.post_merge_window_minutes', 30)` (or 2 clean heartbeats): new feedback tagged to the epic/children, health signals on changed files, first real exercise. Serious regression → **report up** (`feedback_promote`/`decision_ask`), do not expand scope. 3. **Lead → ops handoff** — write a concise watch-note into the epic brief (`brief_append`: what shipped, what to watch, known risks, files touched) + hand ongoing monitoring to ops. Boundary: lead = bounded window for its own feature; ops = standing/ongoing. 4. **Cooperative retire (no hard-kill race)** — lead calls `daemon_winddown_ready` on itself (no role gate, `DaemonRosterService.php:548`) + **stops its heartbeat/work loop** (winddown convention), THEN enqueues `epic_return{action:retire}`. MAIN calls `CloseDaemon::close(lead)` — guard already satisfied, **no forced winddown override / no CloseAllDaemons hard-kill**. Epic brief → `complete`; EnsureDaemons never respawns (leads not in expected sets) → durable retirement. 5. **Failure/reset behavior** — stale/dead lead on an open epic → MAIN decides: respawn a fresh lead (idempotent — durable state in epic brief + children + epic branch) or absorb inline. Lead reset mid-epic → make-before-break handshake; successor inherits `epic_brief_id` + epic worktree + drains its `epic_delegate` mailbox by epic id (baton NOT transferred — lead is a subordinate). Epic cancelled → lead retires without a master merge; branch/worktree torn down. ## Tests - Retire path closes a quiesced lead and marks the epic `complete`. - EnsureDaemons never respawns a retired lead. - Respawn of a fresh lead resumes from the epic brief (durable-state recovery). - Terminal detection counts `abandoned` children. ## Notes - This closes the finite lifecycle. The end-to-end epic can be demonstrated after this slice.

    agent · flower-226-worker
  20. participant joined 1d ago
    system · flower-226-worker

epic · dependencies

Relationships

depends on

agents · waves

Participants

  • flower-226-worker participant · active
  • flower-234-worker participant · active
  • flower-orchestrator participant · active
  • flower-236-worker participant · active
  • system:commit-trailer participant · active

trace · graph

Links

  • Commit #4044 execution

scope

Projects

  • flower · primary

dogfood · read-only

Agent’s-eye view

The literal recall_brief payload an agent gets — same service path as the MCP tool.