diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index 9bbe3feef7..9cc20f758c 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -155,7 +155,10 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
{groups.map((group) => { - const profileAgent = pickProfileAgent(group.agents); + const profileAgent = pickProfileAgent( + group.agents, + group.persona.displayName, + ); return ( ( @@ -272,7 +275,7 @@ function AgentPersonaCard({ onStartAgent: (pubkey: string) => void; onStartPersona: (persona: AgentPersona) => void; }) { - const title = persona.displayName; + const title = agent?.name.trim() || persona.displayName; const modelLabel = resolveAgentCardModelLabel({ agent, personaModel: persona.model, diff --git a/desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs b/desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs new file mode 100644 index 0000000000..b44d5ccff9 --- /dev/null +++ b/desktop/src/features/agents/ui/unifiedAgentGroups.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { pickProfileAgent } from "./unifiedAgentGroups.ts"; + +function agent(name, status = "stopped") { + return { + name, + status, + pid: status === "running" ? 123 : null, + }; +} + +test("pickProfileAgent prefers a configured identity over the persona placeholder", () => { + const selected = pickProfileAgent( + [agent("Bumble"), agent("Maria")], + "Bumble", + ); + + assert.equal(selected.name, "Maria"); +}); + +test("pickProfileAgent still prefers the active runtime", () => { + const selected = pickProfileAgent( + [agent("Bumble", "running"), agent("Maria")], + "Bumble", + ); + + assert.equal(selected.name, "Bumble"); +}); diff --git a/desktop/src/features/agents/ui/unifiedAgentGroups.ts b/desktop/src/features/agents/ui/unifiedAgentGroups.ts index a2d1b987f3..b61e525a8e 100644 --- a/desktop/src/features/agents/ui/unifiedAgentGroups.ts +++ b/desktop/src/features/agents/ui/unifiedAgentGroups.ts @@ -34,11 +34,29 @@ export function buildUnifiedGroups( return { groups, ungrouped, unknown }; } -export function pickProfileAgent(agents: ManagedAgent[]) { +export function pickProfileAgent( + agents: ManagedAgent[], + personaDisplayName?: string, +) { + const placeholderName = personaDisplayName?.trim().toLocaleLowerCase(); return [...agents].sort((left, right) => { const activeDiff = Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left)); if (activeDiff !== 0) return activeDiff; + + // A persona can have both its starter placeholder (for example Bumble) + // and a real configured runtime (for example Maria). Prefer the configured + // identity so the unified card does not hide the agent operators recognise. + if (placeholderName) { + const leftIsPlaceholder = + left.name.trim().toLocaleLowerCase() === placeholderName; + const rightIsPlaceholder = + right.name.trim().toLocaleLowerCase() === placeholderName; + const placeholderDiff = + Number(leftIsPlaceholder) - Number(rightIsPlaceholder); + if (placeholderDiff !== 0) return placeholderDiff; + } + return left.name.localeCompare(right.name); })[0]; } diff --git a/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs b/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs index 4dc5a4ff19..f9d53d08d3 100644 --- a/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs +++ b/desktop/src/features/cos-running-order/lib/cosDeliveryRoom.test.mjs @@ -320,6 +320,21 @@ test("expires the projection at the earliest source or presented evidence deadli } }); +test("ignores invalid evidence when calculating the projection expiry", () => { + const room = projectCosDeliveryRoom(envelope(), { now: NOW }); + room.deliveryRoom.workItems[0].evidence.push({ + ...evidence("unavailable-claim", "unknown"), + observedAt: "", + freshness: "invalid", + freshForMs: 7 * 24 * 60 * 60 * 1_000, + }); + + assert.equal( + cosDeliveryRoomExpiresAt(room), + new Date("2026-07-31T09:19:00.000Z").getTime(), + ); +}); + test("rejects unbounded or unsafe lifetimes and accepts the reviewed boundaries", () => { const hugeSourceLifetime = copy(envelope()); hugeSourceLifetime.source.maxAgeSeconds = 1e308; diff --git a/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts b/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts index 036213717c..8c4a62b3c3 100644 --- a/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts +++ b/desktop/src/features/cos-running-order/lib/cosDeliveryRoomExpiry.ts @@ -116,6 +116,11 @@ export function cosDeliveryRoomExpiresAt(room: CosDeliveryRoom): number { expiresAt(room.source.agentHealth.observedAt, sourceLifetimeMs), ]; const addEvidence = (evidence: DeliveryRoomEvidence) => { + // Invalid evidence is deliberately retained for honest unavailable/unknown + // claims, but it has no verifiable timestamp and therefore cannot define a + // semantic expiry deadline. The parser has already checked that the + // declared freshness agrees with the missing or malformed timestamp. + if (evidence.freshness === "invalid") return; deadlines.push(expiresAt(evidence.observedAt, evidence.freshForMs)); }; diff --git a/mobile/lib/features/cos_running_order/cos_running_order.dart b/mobile/lib/features/cos_running_order/cos_running_order.dart index fcdd282021..81f16df05d 100644 --- a/mobile/lib/features/cos_running_order/cos_running_order.dart +++ b/mobile/lib/features/cos_running_order/cos_running_order.dart @@ -1,110 +1,117 @@ enum CosRunningOrderState { - blocked, - humanTest, - running, - active, ready, - queued, + building, + independentReview, + stagingVerification, completed, } class CosRunningOrderCounts { - final int active; - final int blocked; - final int completed; - final int humanTest; - final int queued; final int ready; - final int running; + final int building; + final int independentReview; + final int stagingVerification; + final int completed; + final int needsManager; + final int blockedOrStalled; const CosRunningOrderCounts({ - required this.active, - required this.blocked, - required this.completed, - required this.humanTest, - required this.queued, required this.ready, - required this.running, + required this.building, + required this.independentReview, + required this.stagingVerification, + required this.completed, + required this.needsManager, + required this.blockedOrStalled, }); - - factory CosRunningOrderCounts.fromJson(Map json) { - return CosRunningOrderCounts( - active: _integer(json['active']), - blocked: _integer(json['blocked']), - completed: _integer(json['completed']), - humanTest: _integer(json['human_test']), - queued: _integer(json['queued']), - ready: _integer(json['ready']), - running: _integer(json['agent_running'] ?? json['running']), - ); - } } class CosRunningOrderItem { final String key; - final String summary; - final String jiraStatus; - final String priority; + final String title; + final String currentActivity; + final String nextAction; + final String owner; final CosRunningOrderState state; - final List blockers; - final bool stagingEvidenced; + final String health; const CosRunningOrderItem({ required this.key, - required this.summary, - required this.jiraStatus, - required this.priority, + required this.title, + required this.currentActivity, + required this.nextAction, + required this.owner, required this.state, - required this.blockers, - required this.stagingEvidenced, + required this.health, }); factory CosRunningOrderItem.fromJson(Map json) { + final reference = _map(json['externalReference']); + final owner = _map(json['owner']); return CosRunningOrderItem( - key: _text(json['key']), - summary: _text(json['summary']), - jiraStatus: _text(json['jira_status']), - priority: _text(json['priority']), - state: _state(json['execution_state'] ?? json['state']), - blockers: _strings(json['blockers']), - stagingEvidenced: json['staging_evidenced'] == true, + key: _text(reference['key']).isEmpty + ? _text(json['id']) + : _text(reference['key']), + title: _text(json['title']), + currentActivity: _text(json['currentActivity']), + nextAction: _text(json['nextAction']), + owner: _text(owner['label']), + state: _state(json['stage']), + health: _text(json['health']), ); } } class CosRunningOrderSnapshot { final DateTime? generatedAt; - final String operationalStatus; - final String overallStatus; - final String? stagingRevision; + final String sourceStatus; final CosRunningOrderCounts counts; final List items; const CosRunningOrderSnapshot({ required this.generatedAt, - required this.operationalStatus, - required this.overallStatus, - required this.stagingRevision, + required this.sourceStatus, required this.counts, required this.items, }); factory CosRunningOrderSnapshot.fromJson(Map json) { - if (json['schema'] != 'mac-workspace/cos-running-order/v1') { - throw const FormatException('Unsupported COS running-order snapshot'); + if (json['schemaVersion'] != 'mac-workspace/delivery-room/v1' || + json['readOnly'] != true) { + throw const FormatException('Unsupported Delivery Room snapshot'); + } + final source = _map(json['source']); + if (source['status'] != 'fresh') { + throw const FormatException('Delivery Room evidence is not current'); } + final room = _map(json['deliveryRoom']); + if (room['schemaVersion'] != 'delivery-room-projection/v1') { + throw const FormatException('Unsupported Delivery Room projection'); + } + final items = _maps(room['workItems']) + .map(CosRunningOrderItem.fromJson) + .where((item) => item.key.isNotEmpty) + .toList(); + int count(CosRunningOrderState state) => + items.where((item) => item.state == state).length; + final attention = _map(room['attention']); return CosRunningOrderSnapshot( - generatedAt: DateTime.tryParse(_text(json['generated_at_utc'])), - operationalStatus: _text(json['operational_status']), - overallStatus: _text(json['overall_status']), - stagingRevision: _text(json['staging_revision']).isEmpty - ? null - : _text(json['staging_revision']), - counts: CosRunningOrderCounts.fromJson(_map(json['counts'])), - items: _maps(json['items']) - .map(CosRunningOrderItem.fromJson) - .where((item) => item.key.isNotEmpty) - .toList(), + generatedAt: DateTime.tryParse(_text(json['generatedAt'])), + sourceStatus: _text(source['status']), + counts: CosRunningOrderCounts( + ready: count(CosRunningOrderState.ready), + building: count(CosRunningOrderState.building), + independentReview: count(CosRunningOrderState.independentReview), + stagingVerification: count(CosRunningOrderState.stagingVerification), + completed: count(CosRunningOrderState.completed), + needsManager: _strings( + _map(attention['needsManager'])['workItemIds'], + ).length, + blockedOrStalled: _strings( + _map(attention['blockedOrStalled'])['workItemIds'], + ).length, + ), + items: items, ); } } @@ -120,37 +127,28 @@ Uri cosRunningOrderUri(String relayUrl) { }; return relay.replace( scheme: scheme, - path: '/api/cos-running-order/v1', + path: '/api/mac-delivery-room/v1', query: null, fragment: null, ); } -Map _map(dynamic value) { - return value is Map ? Map.from(value) : {}; -} +Map _map(dynamic value) => + value is Map ? Map.from(value) : {}; -List> _maps(dynamic value) { - return value is List ? value.map(_map).toList() : []; -} +List> _maps(dynamic value) => + value is List ? value.map(_map).toList() : []; -List _strings(dynamic value) { - return value is List ? value.whereType().toList() : []; -} +List _strings(dynamic value) => + value is List ? value.whereType().toList() : []; String _text(dynamic value) => value is String ? value : ''; -int _integer(dynamic value) => value is int ? value : 0; - -CosRunningOrderState _state(dynamic value) { - return switch (value) { - 'blocked' => CosRunningOrderState.blocked, - 'human-test' => CosRunningOrderState.humanTest, - 'running' => CosRunningOrderState.running, - 'active' => CosRunningOrderState.active, - 'ready' => CosRunningOrderState.ready, - 'queued' => CosRunningOrderState.queued, - 'completed' => CosRunningOrderState.completed, - _ => throw const FormatException('Unsupported COS running-order state'), - }; -} +CosRunningOrderState _state(dynamic value) => switch (value) { + 'ready' => CosRunningOrderState.ready, + 'building' => CosRunningOrderState.building, + 'independent_review' => CosRunningOrderState.independentReview, + 'staging_verification' => CosRunningOrderState.stagingVerification, + 'complete' => CosRunningOrderState.completed, + _ => throw const FormatException('Unsupported Delivery Room stage'), +}; diff --git a/mobile/lib/features/cos_running_order/cos_running_order_page.dart b/mobile/lib/features/cos_running_order/cos_running_order_page.dart index be25c7d927..4e90296df6 100644 --- a/mobile/lib/features/cos_running_order/cos_running_order_page.dart +++ b/mobile/lib/features/cos_running_order/cos_running_order_page.dart @@ -16,12 +16,12 @@ part 'cos_running_order_page/body.dart'; enum _RunningOrderFilter { focus, - blocked, - running, - active, + needsManager, ready, - humanTest, - queued, + building, + review, + verification, + complete, } class CosRunningOrderPage extends StatelessWidget { @@ -29,7 +29,7 @@ class CosRunningOrderPage extends StatelessWidget { @override Widget build(BuildContext context) => const CosWorkspaceModuleGate( - title: 'COS Running Order', + title: 'Delivery Room', requiredModules: ['running_order'], child: _AuthorisedCosRunningOrderPage(), ); @@ -64,7 +64,7 @@ class _AuthorisedCosRunningOrderPage extends HookConsumerWidget { } return FrostedScaffold( - appBar: const FrostedAppBar(title: Text('COS Running Order')), + appBar: const FrostedAppBar(title: Text('Delivery Room')), body: SafeArea( top: false, child: Padding( diff --git a/mobile/lib/features/cos_running_order/cos_running_order_page/body.dart b/mobile/lib/features/cos_running_order/cos_running_order_page/body.dart index 8f33ef160a..7358695fc0 100644 --- a/mobile/lib/features/cos_running_order/cos_running_order_page/body.dart +++ b/mobile/lib/features/cos_running_order/cos_running_order_page/body.dart @@ -18,17 +18,18 @@ class _RunningOrderBody extends StatelessWidget { final items = snapshot.items.where((item) { return switch (filter) { _RunningOrderFilter.focus => - item.state != CosRunningOrderState.queued && - item.state != CosRunningOrderState.completed, - _RunningOrderFilter.blocked => - item.state == CosRunningOrderState.blocked, - _RunningOrderFilter.running => - item.state == CosRunningOrderState.running, - _RunningOrderFilter.active => item.state == CosRunningOrderState.active, + item.state != CosRunningOrderState.completed || + item.health != 'on_track', + _RunningOrderFilter.needsManager => item.health == 'needs_manager', _RunningOrderFilter.ready => item.state == CosRunningOrderState.ready, - _RunningOrderFilter.humanTest => - item.state == CosRunningOrderState.humanTest, - _RunningOrderFilter.queued => item.state == CosRunningOrderState.queued, + _RunningOrderFilter.building => + item.state == CosRunningOrderState.building, + _RunningOrderFilter.review => + item.state == CosRunningOrderState.independentReview, + _RunningOrderFilter.verification => + item.state == CosRunningOrderState.stagingVerification, + _RunningOrderFilter.complete => + item.state == CosRunningOrderState.completed, }; }).toList(); @@ -59,28 +60,28 @@ class _RunningOrderBody extends StatelessWidget { label: 'Focus', ), FilterChipItem( - id: _RunningOrderFilter.blocked, - label: 'Blocked', + id: _RunningOrderFilter.needsManager, + label: 'Needs you', ), FilterChipItem( - id: _RunningOrderFilter.running, - label: 'Agent running', + id: _RunningOrderFilter.building, + label: 'Building', ), FilterChipItem( - id: _RunningOrderFilter.active, - label: 'Jira active', + id: _RunningOrderFilter.review, + label: 'Review', ), FilterChipItem( id: _RunningOrderFilter.ready, label: 'Ready', ), FilterChipItem( - id: _RunningOrderFilter.humanTest, - label: 'Test', + id: _RunningOrderFilter.verification, + label: 'Verify', ), FilterChipItem( - id: _RunningOrderFilter.queued, - label: 'Queue', + id: _RunningOrderFilter.complete, + label: 'Complete', ), ], ), @@ -120,7 +121,7 @@ class _HealthCard extends StatelessWidget { @override Widget build(BuildContext context) { - final healthy = snapshot.operationalStatus == 'ok'; + final healthy = snapshot.sourceStatus == 'fresh'; final colors = context.colors; final generated = snapshot.generatedAt == null ? 'Unknown' @@ -149,21 +150,14 @@ class _HealthCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - healthy ? 'Collector healthy' : 'Collector needs attention', + healthy ? 'Evidence current' : 'Evidence needs attention', style: context.textTheme.titleSmall, ), const SizedBox(height: Grid.quarter), Text( - 'Delivery ${snapshot.overallStatus} · Updated $generated', + 'Read-only delivery view · Updated $generated', style: context.textTheme.bodySmall, ), - if (snapshot.stagingRevision case final revision?) - Text( - 'Staging ${revision.substring(0, revision.length.clamp(0, 12))}', - style: context.textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - ), - ), ], ), ), @@ -184,23 +178,32 @@ class _SummaryGrid extends StatelessWidget { return Row( children: [ Expanded( - child: _SummaryValue(label: 'Blocked', value: counts.blocked), + child: _SummaryValue(label: 'Needs you', value: counts.needsManager), ), const SizedBox(width: Grid.half), Expanded( - child: _SummaryValue(label: 'Agent runs', value: counts.running), + child: _SummaryValue(label: 'Building', value: counts.building), ), const SizedBox(width: Grid.half), Expanded( - child: _SummaryValue(label: 'Jira active', value: counts.active), + child: _SummaryValue( + label: 'Review', + value: counts.independentReview, + ), ), const SizedBox(width: Grid.half), Expanded( - child: _SummaryValue(label: 'Ready', value: counts.ready), + child: _SummaryValue( + label: 'Verify', + value: counts.stagingVerification, + ), ), const SizedBox(width: Grid.half), Expanded( - child: _SummaryValue(label: 'Queued', value: counts.queued), + child: _SummaryValue( + label: 'Blocked', + value: counts.blockedOrStalled, + ), ), ], ); @@ -245,12 +248,10 @@ class _RunningOrderCard extends StatelessWidget { Widget build(BuildContext context) { final colors = context.colors; final stateLabel = switch (item.state) { - CosRunningOrderState.blocked => 'Blocked', - CosRunningOrderState.humanTest => 'Human test', - CosRunningOrderState.running => 'Agent running', - CosRunningOrderState.active => 'Jira active', CosRunningOrderState.ready => 'Ready', - CosRunningOrderState.queued => 'Queued', + CosRunningOrderState.building => 'Building', + CosRunningOrderState.independentReview => 'Independent review', + CosRunningOrderState.stagingVerification => 'Staging verification', CosRunningOrderState.completed => 'Completed', }; return DecoratedBox( @@ -278,28 +279,28 @@ class _RunningOrderCard extends StatelessWidget { ], ), const SizedBox(height: Grid.half), - Text(item.summary, style: context.textTheme.titleSmall), + Text(item.title, style: context.textTheme.titleSmall), const SizedBox(height: Grid.quarter), Text( - '${item.jiraStatus}${item.priority.isEmpty ? '' : ' · ${item.priority}'}', + '${item.owner.isEmpty ? 'Unassigned' : item.owner} · ${item.health.replaceAll('_', ' ')}', style: context.textTheme.bodySmall, ), - for (final blocker in item.blockers) + if (item.currentActivity.isNotEmpty) Padding( padding: const EdgeInsets.only(top: Grid.half), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - LucideIcons.triangleAlert, - size: 16, - color: colors.error, - ), - const SizedBox(width: Grid.half), - Expanded( - child: Text(blocker, style: context.textTheme.bodySmall), - ), - ], + child: Text( + item.currentActivity, + style: context.textTheme.bodyMedium, + ), + ), + if (item.nextAction.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: Grid.half), + child: Text( + 'Next: ${item.nextAction}', + style: context.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + ), ), ), ], @@ -341,7 +342,7 @@ class _RunningOrderError extends StatelessWidget { children: [ const Icon(LucideIcons.triangleAlert), const SizedBox(height: Grid.sm), - const Text('COS running order unavailable'), + const Text('Delivery Room unavailable'), const SizedBox(height: Grid.sm), FilledButton(onPressed: onRetry, child: const Text('Try again')), ], diff --git a/mobile/lib/features/cos_running_order/cos_running_order_provider.dart b/mobile/lib/features/cos_running_order/cos_running_order_provider.dart index 473fdb9224..681a76839d 100644 --- a/mobile/lib/features/cos_running_order/cos_running_order_provider.dart +++ b/mobile/lib/features/cos_running_order/cos_running_order_provider.dart @@ -26,13 +26,11 @@ class CosRunningOrderNotifier extends AsyncNotifier { .get(cosRunningOrderUri(relayUrl)) .timeout(const Duration(seconds: 10)); if (response.statusCode < 200 || response.statusCode >= 300) { - throw StateError( - 'COS running order is unavailable (${response.statusCode})', - ); + throw StateError('Delivery Room is unavailable (${response.statusCode})'); } final payload = jsonDecode(response.body); if (payload is! Map) { - throw const FormatException('COS running order must be a JSON object'); + throw const FormatException('Delivery Room must be a JSON object'); } return CosRunningOrderSnapshot.fromJson(Map.from(payload)); } diff --git a/mobile/lib/features/home/home_page.dart b/mobile/lib/features/home/home_page.dart index 69d4cf1f15..bd489c5de0 100644 --- a/mobile/lib/features/home/home_page.dart +++ b/mobile/lib/features/home/home_page.dart @@ -56,17 +56,17 @@ class HomePage extends HookConsumerWidget { selectedIcon: LucideIcons.search500, label: 'Search', ), + if (workspaceContext?.hasModule('running_order') == true) + const _HomeDestination( + icon: LucideIcons.listChecks300, + selectedIcon: LucideIcons.listChecks500, + label: 'Delivery', + ), if (workspaceContext?.canUseControlRoom == true) const _HomeDestination( icon: LucideIcons.gauge300, selectedIcon: LucideIcons.gauge500, label: 'Control', - ) - else if (workspaceContext?.hasModule('running_order') == true) - const _HomeDestination( - icon: LucideIcons.listChecks300, - selectedIcon: LucideIcons.listChecks500, - label: 'COS', ), if (workspaceContext?.hasModule('my_actions') == true) const _HomeDestination( @@ -79,10 +79,9 @@ class HomePage extends HookConsumerWidget { ChannelsPage(settingsPageBuilder: settingsPageBuilder), const ActivityPage(), const SearchPage(), - if (workspaceContext?.canUseControlRoom == true) - const ControlRoomPage() - else if (workspaceContext?.hasModule('running_order') == true) + if (workspaceContext?.hasModule('running_order') == true) const CosRunningOrderPage(), + if (workspaceContext?.canUseControlRoom == true) const ControlRoomPage(), if (workspaceContext?.hasModule('my_actions') == true) const CosFollowUpPage(), ]; diff --git a/mobile/test/features/cos_running_order/cos_running_order_page_test.dart b/mobile/test/features/cos_running_order/cos_running_order_page_test.dart index 2e340e2c50..d3b556ee5c 100644 --- a/mobile/test/features/cos_running_order/cos_running_order_page_test.dart +++ b/mobile/test/features/cos_running_order/cos_running_order_page_test.dart @@ -29,55 +29,57 @@ void main() { requests += 1; expect( request.url.toString(), - 'https://forge-do.tailfe35cd.ts.net/api/cos-running-order/v1', + 'https://forge-do.tailfe35cd.ts.net/api/mac-delivery-room/v1', ); return http.Response( jsonEncode({ - 'schema': 'mac-workspace/cos-running-order/v1', - 'generated_at_utc': '2026-07-27T16:08:14Z', - 'operational_status': 'ok', - 'overall_status': 'degraded', - 'staging_revision': 'a1b2c3d4e5f678901234567890abcdef', - 'counts': { - 'active': 7, - 'agent_running': 1, - 'blocked': 22, - 'completed': 0, - 'human_test': 1, - 'queued': 96, - 'ready': 0, - 'running': 8, - }, - 'items': [ - { - 'key': 'COS-469', - 'summary': 'Complete the finance workflow', - 'jira_status': 'In Progress', - 'priority': 'High', - 'state': 'blocked', - 'blockers': ['Draft pull request has merge conflicts'], - 'staging_evidenced': false, - }, - { - 'key': 'COS-588', - 'summary': 'Awaiting review in Jira', - 'jira_status': 'In Review', - 'priority': 'Medium', - 'state': 'running', - 'execution_state': 'active', - 'blockers': [], - 'staging_evidenced': false, - }, - { - 'key': 'COS-700', - 'summary': 'Later queued work', - 'jira_status': 'Backlog', - 'priority': 'Low', - 'state': 'queued', - 'blockers': [], - 'staging_evidenced': false, + 'schemaVersion': 'mac-workspace/delivery-room/v1', + 'generatedAt': '2026-07-31T16:08:14Z', + 'readOnly': true, + 'source': {'status': 'fresh'}, + 'deliveryRoom': { + 'schemaVersion': 'delivery-room-projection/v1', + 'attention': { + 'needsManager': { + 'workItemIds': ['COS-469'], + }, + 'blockedOrStalled': { + 'workItemIds': ['COS-469'], + }, }, - ], + 'workItems': [ + { + 'id': 'COS-469', + 'externalReference': {'key': 'COS-469'}, + 'title': 'Complete the finance workflow', + 'currentActivity': 'Draft pull request has merge conflicts.', + 'nextAction': 'Resolve the candidate conflicts.', + 'owner': {'label': 'Marc'}, + 'stage': 'independent_review', + 'health': 'needs_manager', + }, + { + 'id': 'COS-588', + 'externalReference': {'key': 'COS-588'}, + 'title': 'Awaiting review', + 'currentActivity': 'Terra is reviewing the candidate.', + 'nextAction': 'Record the verdict.', + 'owner': {'label': 'Terra reviewer'}, + 'stage': 'independent_review', + 'health': 'on_track', + }, + { + 'id': 'COS-700', + 'externalReference': {'key': 'COS-700'}, + 'title': 'Completed work', + 'currentActivity': 'Verified on staging.', + 'nextAction': '', + 'owner': {'label': 'Hermes'}, + 'stage': 'complete', + 'health': 'on_track', + }, + ], + }, }), 200, ); @@ -101,13 +103,15 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('COS Running Order'), findsOneWidget); - expect(find.text('Collector healthy'), findsOneWidget); - expect(find.textContaining('Delivery degraded'), findsOneWidget); + expect(find.text('Delivery Room'), findsOneWidget); + expect(find.text('Evidence current'), findsOneWidget); + expect(find.textContaining('Read-only delivery view'), findsOneWidget); expect(find.text('COS-469'), findsOneWidget); - expect(find.text('Draft pull request has merge conflicts'), findsOneWidget); - expect(find.text('Jira active'), findsWidgets); - expect(find.text('COS-588'), findsOneWidget); + expect( + find.text('Draft pull request has merge conflicts.'), + findsOneWidget, + ); + expect(find.text('Review'), findsWidgets); expect(find.text('COS-700'), findsNothing); final container = ProviderScope.containerOf( diff --git a/mobile/test/features/cos_running_order/cos_running_order_test.dart b/mobile/test/features/cos_running_order/cos_running_order_test.dart index b3b743e319..144e36a139 100644 --- a/mobile/test/features/cos_running_order/cos_running_order_test.dart +++ b/mobile/test/features/cos_running_order/cos_running_order_test.dart @@ -2,68 +2,74 @@ import 'package:buzz/features/cos_running_order/cos_running_order.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('parses the stable Forge running-order contract', () { + test('parses the signed Delivery Room projection contract', () { final snapshot = CosRunningOrderSnapshot.fromJson({ - 'schema': 'mac-workspace/cos-running-order/v1', - 'generated_at_utc': '2026-07-27T16:08:14Z', - 'operational_status': 'ok', - 'overall_status': 'degraded', - 'staging_revision': '9c351c0ce66071cf2380edcc31e413d176f0b3d2', - 'counts': { - 'active': 1, - 'agent_running': 1, - 'blocked': 1, - 'completed': 0, - 'human_test': 0, - 'queued': 1, - 'ready': 0, - 'running': 2, - }, - 'items': [ - { - 'key': 'COS-102', - 'summary': 'Blocked work', - 'jira_status': 'In Progress', - 'priority': 'High', - 'state': 'blocked', - 'blockers': ['PR #22 has failed checks'], - 'admission_signals': ['forge-ready'], - 'pull_requests': [ - {'number': 22, 'state': 'OPEN', 'draft': false}, - ], - 'active_run': null, - 'staging_evidenced': false, - }, - { - 'key': 'COS-103', - 'summary': 'Active in Jira', - 'jira_status': 'In Progress', - 'priority': 'Medium', - 'state': 'running', - 'execution_state': 'active', - 'blockers': [], - 'staging_evidenced': false, + 'schemaVersion': 'mac-workspace/delivery-room/v1', + 'generatedAt': '2026-07-31T16:08:14Z', + 'readOnly': true, + 'source': {'status': 'fresh'}, + 'deliveryRoom': { + 'schemaVersion': 'delivery-room-projection/v1', + 'attention': { + 'needsManager': { + 'workItemIds': ['COS-102'], + }, + 'blockedOrStalled': { + 'workItemIds': ['COS-102'], + }, }, - ], + 'workItems': [ + { + 'id': 'work-cos-102', + 'externalReference': {'key': 'COS-102'}, + 'title': 'Complete the finance workflow', + 'currentActivity': 'Terra is reviewing the candidate.', + 'nextAction': 'Address the review verdict.', + 'owner': {'label': 'Terra reviewer'}, + 'stage': 'independent_review', + 'health': 'needs_manager', + }, + { + 'id': 'work-cos-103', + 'externalReference': {'key': 'COS-103'}, + 'title': 'Verify the staging release', + 'currentActivity': 'Browser smoke is running.', + 'nextAction': 'Record the staging evidence.', + 'owner': {'label': 'Hermes supervisor'}, + 'stage': 'staging_verification', + 'health': 'on_track', + }, + ], + }, }); - expect(snapshot.counts.active, 1); - expect(snapshot.counts.blocked, 1); + expect(snapshot.counts.independentReview, 1); + expect(snapshot.counts.stagingVerification, 1); + expect(snapshot.counts.needsManager, 1); expect(snapshot.items.first.key, 'COS-102'); - expect(snapshot.items.first.blockers, ['PR #22 has failed checks']); - expect(snapshot.items.last.key, 'COS-103'); - expect(snapshot.items.last.state, CosRunningOrderState.active); - expect(snapshot.stagingRevision, startsWith('9c351c0c')); + expect(snapshot.items.first.state, CosRunningOrderState.independentReview); + expect(snapshot.sourceStatus, 'fresh'); + }); + + test('fails closed for stale Delivery Room evidence', () { + expect( + () => CosRunningOrderSnapshot.fromJson({ + 'schemaVersion': 'mac-workspace/delivery-room/v1', + 'readOnly': true, + 'source': {'status': 'stale'}, + }), + throwsFormatException, + ); }); - test('derives the adapter URL from the active community relay', () { + test('derives the Delivery Room URL from the active community relay', () { expect( cosRunningOrderUri('wss://forge-do.tailfe35cd.ts.net/').toString(), - 'https://forge-do.tailfe35cd.ts.net/api/cos-running-order/v1', + 'https://forge-do.tailfe35cd.ts.net/api/mac-delivery-room/v1', ); expect( cosRunningOrderUri('https://forge-do.tailfe35cd.ts.net/').toString(), - 'https://forge-do.tailfe35cd.ts.net/api/cos-running-order/v1', + 'https://forge-do.tailfe35cd.ts.net/api/mac-delivery-room/v1', ); }); } diff --git a/mobile/test/features/home/home_page_test.dart b/mobile/test/features/home/home_page_test.dart index 5367a0f544..5f055842ff 100644 --- a/mobile/test/features/home/home_page_test.dart +++ b/mobile/test/features/home/home_page_test.dart @@ -62,11 +62,13 @@ void main() { expect(find.text('Activity'), findsNothing); expect(find.text('Search'), findsNothing); expect(find.text('Control'), findsNothing); + expect(find.text('Delivery'), findsNothing); expect(find.text('My Actions'), findsNothing); expect(find.bySemanticsLabel('Home'), findsOneWidget); expect(find.bySemanticsLabel('Activity'), findsOneWidget); expect(find.bySemanticsLabel('Search'), findsOneWidget); expect(find.bySemanticsLabel('Control'), findsOneWidget); + expect(find.bySemanticsLabel('Delivery'), findsOneWidget); expect(find.bySemanticsLabel('My Actions'), findsOneWidget); final quickAction = find.byTooltip('Create or start conversation'); @@ -101,7 +103,7 @@ void main() { await tester.pumpAndSettle(); expect(find.bySemanticsLabel('Control'), findsNothing); - expect(find.bySemanticsLabel('COS'), findsNothing); + expect(find.bySemanticsLabel('Delivery'), findsNothing); expect(find.bySemanticsLabel('My Actions'), findsNothing); expect(find.bySemanticsLabel('Home'), findsOneWidget); expect(find.bySemanticsLabel('Activity'), findsOneWidget);