Skip to content

Commit b0b579e

Browse files
committed
ROADMAP ultraworkers#133: Blocked-state subphase contract — implement §6.5
Adds BlockedSubphase enum with 7 variants for structured blocked-state reporting: - blocked.trust_prompt — trust gate blockers - blocked.prompt_delivery — prompt misdelivery - blocked.plugin_init — plugin startup failures - blocked.mcp_handshake — MCP connection issues - blocked.branch_freshness — stale branch blockers - blocked.test_hang — test timeout/hang - blocked.report_pending — report generation stuck LaneEventBlocker now carries optional subphase field that gets serialized into LaneEvent data. Enables clawhip to route recovery without pane scraping. Updates: - lane_events.rs: BlockedSubphase enum, LaneEventBlocker.subphase field - lane_events.rs: blocked()/failed() constructors with subphase serialization - lib.rs: Export BlockedSubphase - tools/src/lib.rs: classify_lane_blocker() with subphase: None - Test imports and fixtures updated Backward-compatible: subphase is Option<>, existing events continue to work.
1 parent c956f78 commit b0b579e

4 files changed

Lines changed: 49 additions & 11 deletions

File tree

ROADMAP.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,12 @@ Acceptance:
803803
- channel status updates stay short and machine-grounded
804804
- claws stop inferring state from raw build spam
805805

806-
### 6.5. Blocked-state subphase contract
806+
### 133. Blocked-state subphase contract (was §6.5)
807+
**Filed:** 2026-04-20 from dogfood cycle — previous cycle identified §4.44.5 provenance gap, this cycle targets §6.5 implementation.
808+
809+
**Problem:** Currently `lane.blocked` is a single opaque state. Recovery recipes cannot distinguish trust-gate blockers from MCP handshake failures, branch freshness issues, or test hangs. All blocked lanes look the same, forcing pane-scrape triage.
810+
811+
**Concrete implementation:
807812
When a lane is `blocked`, also expose the exact subphase where progress stopped, rather than forcing claws to infer from logs.
808813

809814
Subphases should include at least:

rust/crates/runtime/src/lane_events.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,31 @@ pub fn dedupe_terminal_events(events: &[LaneEvent]) -> Vec<LaneEvent> {
383383
result
384384
}
385385

386+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387+
pub enum BlockedSubphase {
388+
#[serde(rename = "blocked.trust_prompt")]
389+
TrustPrompt { gate_repo: String },
390+
#[serde(rename = "blocked.prompt_delivery")]
391+
PromptDelivery { attempt: u32 },
392+
#[serde(rename = "blocked.plugin_init")]
393+
PluginInit { plugin_name: String },
394+
#[serde(rename = "blocked.mcp_handshake")]
395+
McpHandshake { server_name: String, attempt: u32 },
396+
#[serde(rename = "blocked.branch_freshness")]
397+
BranchFreshness { behind_main: u32 },
398+
#[serde(rename = "blocked.test_hang")]
399+
TestHang { elapsed_secs: u32, test_name: Option<String> },
400+
#[serde(rename = "blocked.report_pending")]
401+
ReportPending { since_secs: u32 },
402+
}
403+
386404
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387405
pub struct LaneEventBlocker {
388406
#[serde(rename = "failureClass")]
389407
pub failure_class: LaneFailureClass,
390408
pub detail: String,
409+
#[serde(skip_serializing_if = "Option::is_none")]
410+
pub subphase: Option<BlockedSubphase>,
391411
}
392412

393413
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -487,16 +507,24 @@ impl LaneEvent {
487507

488508
#[must_use]
489509
pub fn blocked(emitted_at: impl Into<String>, blocker: &LaneEventBlocker) -> Self {
490-
Self::new(LaneEventName::Blocked, LaneEventStatus::Blocked, emitted_at)
510+
let mut event = Self::new(LaneEventName::Blocked, LaneEventStatus::Blocked, emitted_at)
491511
.with_failure_class(blocker.failure_class)
492-
.with_detail(blocker.detail.clone())
512+
.with_detail(blocker.detail.clone());
513+
if let Some(ref subphase) = blocker.subphase {
514+
event = event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
515+
}
516+
event
493517
}
494518

495519
#[must_use]
496520
pub fn failed(emitted_at: impl Into<String>, blocker: &LaneEventBlocker) -> Self {
497-
Self::new(LaneEventName::Failed, LaneEventStatus::Failed, emitted_at)
521+
let mut event = Self::new(LaneEventName::Failed, LaneEventStatus::Failed, emitted_at)
498522
.with_failure_class(blocker.failure_class)
499-
.with_detail(blocker.detail.clone())
523+
.with_detail(blocker.detail.clone());
524+
if let Some(ref subphase) = blocker.subphase {
525+
event = event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
526+
}
527+
event
500528
}
501529

502530
#[must_use]
@@ -570,9 +598,9 @@ mod tests {
570598

571599
use super::{
572600
compute_event_fingerprint, dedupe_superseded_commit_events, dedupe_terminal_events,
573-
is_terminal_event, EventProvenance, LaneCommitProvenance, LaneEvent, LaneEventBlocker,
574-
LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus, LaneFailureClass,
575-
LaneOwnership, SessionIdentity, WatcherAction,
601+
is_terminal_event, BlockedSubphase, EventProvenance, LaneCommitProvenance, LaneEvent,
602+
LaneEventBlocker, LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus,
603+
LaneFailureClass, LaneOwnership, SessionIdentity, WatcherAction,
576604
};
577605

578606
#[test]
@@ -641,6 +669,10 @@ mod tests {
641669
let blocker = LaneEventBlocker {
642670
failure_class: LaneFailureClass::McpStartup,
643671
detail: "broken server".to_string(),
672+
subphase: Some(BlockedSubphase::McpHandshake {
673+
server_name: "test-server".to_string(),
674+
attempt: 1,
675+
}),
644676
};
645677

646678
let blocked = LaneEvent::blocked("2026-04-04T00:00:00Z", &blocker);

rust/crates/runtime/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ pub use hooks::{
8484
};
8585
pub use lane_events::{
8686
compute_event_fingerprint, dedupe_superseded_commit_events, dedupe_terminal_events,
87-
is_terminal_event, EventProvenance, LaneCommitProvenance, LaneEvent, LaneEventBlocker,
88-
LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus, LaneFailureClass,
89-
LaneOwnership, SessionIdentity, WatcherAction,
87+
is_terminal_event, BlockedSubphase, EventProvenance, LaneCommitProvenance, LaneEvent,
88+
LaneEventBlocker, LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus,
89+
LaneFailureClass, LaneOwnership, SessionIdentity, WatcherAction,
9090
};
9191
pub use mcp::{
9292
mcp_server_signature, mcp_tool_name, mcp_tool_prefix, normalize_name_for_mcp,

rust/crates/tools/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4459,6 +4459,7 @@ fn classify_lane_blocker(error: &str) -> LaneEventBlocker {
44594459
LaneEventBlocker {
44604460
failure_class: classify_lane_failure(error),
44614461
detail,
4462+
subphase: None,
44624463
}
44634464
}
44644465

0 commit comments

Comments
 (0)