feat(scheduling): negotiation loop Slice A — propose-and-confirm displacement#120
Merged
Conversation
Design-ready architecture for the deferred constraint-aware negotiation/rearrange loop from the Scheduling Fable critique. When a hard-deadline task can't fit before its deadline, instead of stranding in needs_scheduling, Scheduler.negotiate_placement displaces the minimum-cost set of MOVABLE Donna-managed events (lowest-priority, most-flexible) and re-places them into free slots; a cascade_shift primitive handles overruns. Two non-negotiable invariants: never move user-created events; never silently violate a hard deadline. Termination is structural (displaced tasks re-place into FREE slots → cannot re-displace). Ships propose-and-confirm by default (honors the 2026-06-05 committed-item-confirmation invariant); auto-apply is config-gated. Verified against code: needs_scheduling->scheduled (alternative_or_rearrange_accepted) + needs_scheduling->backlog transitions already exist in task_states.yaml (no state machine change needed); CalendarEvent carries donna_managed + donna_task_id. Builds on the S1 bundle's Scheduler._lock, fail-closed _gather_busy, tz-aware find_next_slot. Includes: displacement cost function, failure->recovery matrix, config contract, 3-slice phasing (single-displacement confirm-only -> multi+auto-apply -> cascade), test plan, and 6 open owner decisions. No code yet; pending owner decisions.
…lacement Implements Slice A of the scheduling negotiation design (Plan 2): when a hard-deadline task can't fit before its deadline, instead of just parking it in needs_scheduling, propose displacing the minimum-cost set of MOVABLE Donna-managed events so it fits — propose-and-confirm only (no silent auto-apply). - _iter_window_valid_slots refactor: extracted the window-valid candidate generator from find_next_slot; find_next_slot is now 'first yielded slot with zero overlaps' (behavior unchanged — the 22 existing scheduler tests pass as-is). - Scheduler.negotiate_placement: fail-closed busy union; movable-blocker scan; cost- ranked candidates; victims re-place via find_next_slot into FREE slots (structural depth-1 termination — a displaced task can never re-displace). Hard-deadline guard: rejects any slot ending past the deadline, so a hard deadline is never silently blown (for T and, via find_next_slot's own clamp, for each victim). - Movability filter: user (non-donna_managed) events and read-only-calendar events are UNCONDITIONALLY immovable (no override); victim must be scheduled, lower priority (conservative OD-1 default), past min_lead, under the per-day auto-move cap. - negotiate_and_apply (under Scheduler._lock): Slice A always persists the proposal + returns PROPOSED. _apply (accept path) re-validates under the lock (re-reads busy, verifies move.old/new/T-slot; re-negotiates once on drift, applies only if <= the approved cost). Moves before T's create; victims stay scheduled, reschedule_count+1; T uses the existing needs_scheduling->scheduled transition (no state-machine change). - negotiation_proposals table (alembic f5a6b7c8d9e0) + repo + ORM; NegotiationConfig + calendar.yaml block (conservative defaults); NegotiationProposalView (Accept/Decline/ Pick-time) + NOTIF_RESCHEDULE; auto_scheduler NoSlotFoundError hook + §2 dispatch matrix. Deferred to Slices B/C (extension points left): cascade_shift, overrun detector, silent auto_apply, multi-displacement. Spec drift logged as NEG-A in followups.md (§6.1.2 auto-move tables to be rewritten when auto-apply ships). Design + verification: docs/superpowers/specs/2026-06-12-scheduling-negotiation-design.md; domain doc updated. Reviewed the safety-critical pieces (movability filter, structural termination, fail- closed, under-lock re-validation). Tests: 78 passed (incl. 27 new negotiation tests); ruff + mypy clean; migration up/down/up clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
When a hard-deadline task can't fit before its deadline, the scheduler today just parks it in
needs_scheduling. This adds Slice A of the scheduling negotiation design: Donna instead proposes displacing the minimum-cost set of movable Donna-managed events so the task fits — and asks the user to Accept / Decline / Pick-time via Discord. No silent moves.Design:
docs/superpowers/specs/2026-06-12-scheduling-negotiation-design.md. Realizes theneeds_schedulingrearrange loop left as future work indocs/domain/scheduling.md; calendar-write serialization perspec_v3.md §3.7.1. Spec drift logged asNEG-Aindocs/superpowers/specs/followups.md(the§6.1.2silent-auto-move conflict tables get rewritten when auto-apply ships in a later slice).Safety invariants (reviewed line-by-line)
donna_managedevents on the personal write calendar are eligible; user-created events and anything on a read-only calendar are unconditionally immovable — no override knob (_movable, §1.3).find_next_slot, so a moved task can never re-displace anything — no recursion.Scheduler._lock; a calendar-read failure aborts placement (CalendarReadError) rather than booking blind. Accept re-validates under the lock (re-reads busy, re-checks each move + T's slot; re-negotiates once on drift and applies only if ≤ the approved cost, else re-proposes).find_next_slotbehavior unchanged — the window-valid candidate generator was extracted (_iter_window_valid_slots) andfind_next_slotis now "first yielded slot with zero overlaps"; the 22 existing scheduler tests pass as-is.Scope
Slice A is propose-and-confirm only. Deferred to Slices B/C (extension points left in place):
cascade_shift, the overrun detector, multi-displacement (>1 victim chains), and config-gated silentauto_apply. Config lives undercalendar.yamlnegotiation:with conservative defaults (OD-1…OD-6).Changes
scheduling/scheduler.py:_iter_window_valid_slotsrefactor;negotiate_placement,_movable,_auto_moves_today, cost fns;negotiate_and_apply+_apply(under-lock re-validation).negotiation_proposalstable — Alembicf5a6b7c8d9e0(down_revision933f74c55f24, single head) + repo methods +NegotiationProposalORM.config.py+config/calendar.yaml:NegotiationConfigblock.integrations/discord_views.py:NegotiationProposalView(Accept/Decline/Pick-time);notifications/service.py:NOTIF_RESCHEDULE.scheduling/auto_scheduler.py:NoSlotFoundErrorhook + §2 dispatch matrix (PROPOSED → Discord view, IMPOSSIBLE → never-silent options, CalendarReadError → fallback alert).Verification
test_scheduler.py+scheduling/+test_auto_scheduler.py+ persistence) — 27 new negotiation tests.https://claude.ai/code/session_01ChAGsS8vJGrz44ojrASLYs
Generated by Claude Code