Status: APPROVED (2026-03-19, Chief Architect) Issue: #922 Date: 2026-03-19 Decision Makers: Lead Developer, Chief Architect, PM
Piper Morgan has three independent systems for offering workflows to users and detecting acceptance, plus a fourth mechanism for resume offers. These were built at different times for different issues and never unified:
| System | Origin | Detection Method | Pipeline Location |
|---|---|---|---|
| Soft Offer (#824) | Proactive workflow suggestions | detect_offer_response() via ACCEPT_PATTERNS |
Line 448 in intent_service.py |
| Onboarding Offer (#888) | First-time user setup | handle_offer_response() via CONFIRM_PATTERNS |
Line 596 in intent_service.py |
| Contextual Offer (#852) | “Would you like to know more?” | detect_offer_response() (shared with soft) |
Line 530 in intent_service.py |
| Resume Offer | Suspended process resumption | Frozenset matching | Line 606 in intent_service.py |
#922 (Critical UX Bug): When the system offers “Would you like me to help you set up your project portfolio?” and the user says “Sure”, the soft offer system consumes the affirmation first (line 448 runs before line 596). But soft offer’s switch statement at line 449 only has a real handler for meeting — for project_setup, it returns “Let’s get things organized” and does nothing. The onboarding system at line 596 never fires.
This is an instance of the “extension without integration” pattern (see methodological note, 2026-03-16): project_setup was added to the workflow type map but the acceptance path was never completed.
The same structural failure — extending one layer without verifying downstream layers — has now caused 6 bugs (#915, #916, #918, #919, #922, plus the onboarding dead-end). The root cause is that each offer system has its own detection logic, its own pipeline position, its own acceptance handling, and no shared contract.
PM has directed (2026-03-19):
Comment out / disable all onboarding workflow code:
services/onboarding/portfolio_handler.py — handler, state machine, turn processingservices/process/adapters.py — OnboardingProcessAdapter registrationservices/process/registry.py — unregister onboarding at priority 10# ADR-059: onboarding on ice commentRationale: Onboarding is one of three competing offer/acceptance mechanisms. It has its own state machine (OFFERED → ACTIVE → SUSPENDED → COMPLETED), its own pattern detection, its own pipeline hook. Removing it eliminates one competing system and simplifies the remaining consolidation. If the onboarding experience is wanted later, it can be re-enabled on top of the unified dispatcher.
Replace the if workflow_type == "meeting" switch in soft offer acceptance with a registry-based dispatcher:
# services/intent_service/workflow_dispatcher.py
WORKFLOW_REGISTRY: dict[str, WorkflowEntry] = {
"meeting": WorkflowEntry(
entry_point=start_slot_filling,
requires_context=["attendees", "time"],
),
# Future entries added here — no switch statement modification needed
}
def dispatch_workflow(workflow_type: str, session_id: str, **kwargs) -> IntentProcessingResult:
entry = WORKFLOW_REGISTRY.get(workflow_type)
if entry is None:
# No handler → route to floor with context
return route_to_floor(workflow_type, session_id)
return entry.entry_point(session_id, **kwargs)
Design principles (informed by OpenClaw Gateway pattern):
workflow_type → entry_point, nothing elseAssessment: These serve genuinely different purposes:
Decision: Keep both, but unify acceptance detection:
detect_offer_response() (already shared between soft and contextual)Q1: Should the workflow dispatcher be a new component (services/intent_service/workflow_dispatcher.py) or folded into the existing WorkflowOfferService in soft_invocation.py?
Lead Dev recommendation: New component. WorkflowOfferService handles offer presentation (should_offer, format_offer, throttling). The dispatcher handles offer acceptance routing. These are different concerns. Keeping them separate follows the same pattern as action_registry.py (disposition lookup) being separate from intent_service.py (dispatch).
Q2: After removing onboarding, the guided process registry (services/process/registry.py) still has ONBOARDING at priority 10. Should we:
Lead Dev recommendation: Option (c) long-term, option (a) for now. The dispatcher should be able to launch workflows that become guided processes, but that’s a future concern. For now, just remove the dead registration.
Q3: The resume offer mechanism (line 606) detects suspended processes and offers to resume them. Should this go through the workflow dispatcher, or remain separate?
Lead Dev recommendation: Route through dispatcher. A resume is just “start workflow X with pre-existing state.” The dispatcher entry point can accept a resume_session parameter.
detect_offer_response() timing (line 448) still works when dispatch is decoupled from detectiondispatch_workflow() — ~1 hourTotal: ~5 hours estimated
dev/active/methodological-note-classification-handling-contract.mdmailboxes/cio/inbox/cover-note-classification-handling-contract-2026-03-16.md