@a5c-ai/krate 5.0.1-staging.c74b36109 → 5.0.1-staging.ceeaa92b2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@
19
19
  "platformNamespace": "krate-org-default"
20
20
  }
21
21
  ],
22
- "generatedAt": "2026-05-17T05:48:07.189Z",
22
+ "generatedAt": "2026-05-17T13:50:50.181Z",
23
23
  "correlationId": null,
24
24
  "controller": {
25
25
  "mode": "krate-workspace",
@@ -583,7 +583,7 @@
583
583
  "maintainers"
584
584
  ],
585
585
  "invitedBy": "admin",
586
- "expiresAt": "2026-05-24T05:48:07.187Z"
586
+ "expiresAt": "2026-05-24T13:50:50.179Z"
587
587
  },
588
588
  "status": {
589
589
  "phase": "Pending",
@@ -595,7 +595,7 @@
595
595
  "Pending": 1
596
596
  },
597
597
  "storage": "etcd",
598
- "yaml": "apiVersion: krate.a5c.ai/v1alpha1\nkind: Invite\nmetadata:\n namespace: krate-org-default\n labels:\n role: member\n annotations:\n name: new-user-example-com\n resourceVersion: 1\nspec:\n organizationRef: default\n email: new-user@example.com\n role: member\n teams:\n - maintainers\n invitedBy: admin\n expiresAt: 2026-05-24T05:48:07.187Z\nstatus:\n phase: Pending\n storage: etcd\n",
598
+ "yaml": "apiVersion: krate.a5c.ai/v1alpha1\nkind: Invite\nmetadata:\n namespace: krate-org-default\n labels:\n role: member\n annotations:\n name: new-user-example-com\n resourceVersion: 1\nspec:\n organizationRef: default\n email: new-user@example.com\n role: member\n teams:\n - maintainers\n invitedBy: admin\n expiresAt: 2026-05-24T13:50:50.179Z\nstatus:\n phase: Pending\n storage: etcd\n",
599
599
  "action": {
600
600
  "list": "Open Invite records in krate-org-default",
601
601
  "watch": "Watch Invite updates in krate-org-default",
@@ -2476,7 +2476,7 @@
2476
2476
  "maintainers"
2477
2477
  ],
2478
2478
  "phase": "Pending",
2479
- "expiresAt": "2026-05-24T05:48:07.187Z"
2479
+ "expiresAt": "2026-05-24T13:50:50.179Z"
2480
2480
  }
2481
2481
  ],
2482
2482
  "mappings": [
@@ -3082,7 +3082,7 @@
3082
3082
  "maintainers"
3083
3083
  ],
3084
3084
  "phase": "Pending",
3085
- "expiresAt": "2026-05-24T05:48:07.187Z"
3085
+ "expiresAt": "2026-05-24T13:50:50.179Z"
3086
3086
  }
3087
3087
  ],
3088
3088
  "mappings": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "project": "Krate",
3
3
  "version": "0.1.0",
4
- "generatedAt": "2026-05-17T05:48:07.197Z",
4
+ "generatedAt": "2026-05-17T13:50:50.189Z",
5
5
  "status": "ready-for-local-development",
6
6
  "components": [
7
7
  {
@@ -465,7 +465,7 @@
465
465
  "maintainers"
466
466
  ],
467
467
  "invitedBy": "admin",
468
- "expiresAt": "2026-05-24T05:48:07.187Z"
468
+ "expiresAt": "2026-05-24T13:50:50.179Z"
469
469
  },
470
470
  "status": {
471
471
  "phase": "Pending",
@@ -547,7 +547,7 @@
547
547
  },
548
548
  "auditLog": [
549
549
  {
550
- "at": "2026-05-17T05:48:07.185Z",
550
+ "at": "2026-05-17T13:50:50.177Z",
551
551
  "operation": "create",
552
552
  "user": "admin@example.com",
553
553
  "groups": [
@@ -559,7 +559,7 @@
559
559
  "allowed": true
560
560
  },
561
561
  {
562
- "at": "2026-05-17T05:48:07.185Z",
562
+ "at": "2026-05-17T13:50:50.178Z",
563
563
  "operation": "create",
564
564
  "user": "admin@example.com",
565
565
  "groups": [
@@ -571,7 +571,7 @@
571
571
  "allowed": true
572
572
  },
573
573
  {
574
- "at": "2026-05-17T05:48:07.186Z",
574
+ "at": "2026-05-17T13:50:50.178Z",
575
575
  "operation": "create",
576
576
  "user": "admin@example.com",
577
577
  "groups": [
@@ -583,7 +583,7 @@
583
583
  "allowed": true
584
584
  },
585
585
  {
586
- "at": "2026-05-17T05:48:07.186Z",
586
+ "at": "2026-05-17T13:50:50.178Z",
587
587
  "operation": "create",
588
588
  "user": "platform@example.com",
589
589
  "groups": [
@@ -595,7 +595,7 @@
595
595
  "allowed": true
596
596
  },
597
597
  {
598
- "at": "2026-05-17T05:48:07.186Z",
598
+ "at": "2026-05-17T13:50:50.178Z",
599
599
  "operation": "create",
600
600
  "user": "admin@example.com",
601
601
  "groups": [
@@ -607,7 +607,7 @@
607
607
  "allowed": true
608
608
  },
609
609
  {
610
- "at": "2026-05-17T05:48:07.186Z",
610
+ "at": "2026-05-17T13:50:50.178Z",
611
611
  "operation": "create",
612
612
  "user": "platform@example.com",
613
613
  "groups": [
@@ -619,7 +619,7 @@
619
619
  "allowed": true
620
620
  },
621
621
  {
622
- "at": "2026-05-17T05:48:07.186Z",
622
+ "at": "2026-05-17T13:50:50.178Z",
623
623
  "operation": "create",
624
624
  "user": "platform@example.com",
625
625
  "groups": [
@@ -631,7 +631,7 @@
631
631
  "allowed": true
632
632
  },
633
633
  {
634
- "at": "2026-05-17T05:48:07.187Z",
634
+ "at": "2026-05-17T13:50:50.178Z",
635
635
  "operation": "create",
636
636
  "user": "platform@example.com",
637
637
  "groups": [
@@ -643,7 +643,7 @@
643
643
  "allowed": true
644
644
  },
645
645
  {
646
- "at": "2026-05-17T05:48:07.187Z",
646
+ "at": "2026-05-17T13:50:50.179Z",
647
647
  "operation": "create",
648
648
  "user": "admin@example.com",
649
649
  "groups": [
@@ -655,7 +655,7 @@
655
655
  "allowed": true
656
656
  },
657
657
  {
658
- "at": "2026-05-17T05:48:07.187Z",
658
+ "at": "2026-05-17T13:50:50.179Z",
659
659
  "operation": "create",
660
660
  "user": "platform@example.com",
661
661
  "groups": [
@@ -667,7 +667,7 @@
667
667
  "allowed": true
668
668
  },
669
669
  {
670
- "at": "2026-05-17T05:48:07.187Z",
670
+ "at": "2026-05-17T13:50:50.179Z",
671
671
  "operation": "create",
672
672
  "user": "platform@example.com",
673
673
  "groups": [
@@ -915,7 +915,7 @@
915
915
  }
916
916
  },
917
917
  "audit": {
918
- "at": "2026-05-17T05:48:07.185Z",
918
+ "at": "2026-05-17T13:50:50.177Z",
919
919
  "operation": "create",
920
920
  "user": "admin@example.com",
921
921
  "groups": [
@@ -956,7 +956,7 @@
956
956
  }
957
957
  },
958
958
  "audit": {
959
- "at": "2026-05-17T05:48:07.185Z",
959
+ "at": "2026-05-17T13:50:50.178Z",
960
960
  "operation": "create",
961
961
  "user": "admin@example.com",
962
962
  "groups": [
@@ -992,7 +992,7 @@
992
992
  }
993
993
  },
994
994
  "audit": {
995
- "at": "2026-05-17T05:48:07.186Z",
995
+ "at": "2026-05-17T13:50:50.178Z",
996
996
  "operation": "create",
997
997
  "user": "admin@example.com",
998
998
  "groups": [
@@ -1037,7 +1037,7 @@
1037
1037
  }
1038
1038
  },
1039
1039
  "audit": {
1040
- "at": "2026-05-17T05:48:07.186Z",
1040
+ "at": "2026-05-17T13:50:50.178Z",
1041
1041
  "operation": "create",
1042
1042
  "user": "platform@example.com",
1043
1043
  "groups": [
@@ -1081,7 +1081,7 @@
1081
1081
  }
1082
1082
  },
1083
1083
  "audit": {
1084
- "at": "2026-05-17T05:48:07.186Z",
1084
+ "at": "2026-05-17T13:50:50.178Z",
1085
1085
  "operation": "create",
1086
1086
  "user": "admin@example.com",
1087
1087
  "groups": [
@@ -1129,7 +1129,7 @@
1129
1129
  }
1130
1130
  },
1131
1131
  "audit": {
1132
- "at": "2026-05-17T05:48:07.186Z",
1132
+ "at": "2026-05-17T13:50:50.178Z",
1133
1133
  "operation": "create",
1134
1134
  "user": "platform@example.com",
1135
1135
  "groups": [
@@ -1179,7 +1179,7 @@
1179
1179
  }
1180
1180
  },
1181
1181
  "audit": {
1182
- "at": "2026-05-17T05:48:07.186Z",
1182
+ "at": "2026-05-17T13:50:50.178Z",
1183
1183
  "operation": "create",
1184
1184
  "user": "platform@example.com",
1185
1185
  "groups": [
@@ -1230,7 +1230,7 @@
1230
1230
  }
1231
1231
  },
1232
1232
  "audit": {
1233
- "at": "2026-05-17T05:48:07.187Z",
1233
+ "at": "2026-05-17T13:50:50.178Z",
1234
1234
  "operation": "create",
1235
1235
  "user": "platform@example.com",
1236
1236
  "groups": [
@@ -1265,7 +1265,7 @@
1265
1265
  "maintainers"
1266
1266
  ],
1267
1267
  "invitedBy": "admin",
1268
- "expiresAt": "2026-05-24T05:48:07.187Z"
1268
+ "expiresAt": "2026-05-24T13:50:50.179Z"
1269
1269
  },
1270
1270
  "status": {
1271
1271
  "phase": "Pending",
@@ -1273,7 +1273,7 @@
1273
1273
  }
1274
1274
  },
1275
1275
  "audit": {
1276
- "at": "2026-05-17T05:48:07.187Z",
1276
+ "at": "2026-05-17T13:50:50.179Z",
1277
1277
  "operation": "create",
1278
1278
  "user": "admin@example.com",
1279
1279
  "groups": [
@@ -1324,7 +1324,7 @@
1324
1324
  }
1325
1325
  },
1326
1326
  "audit": {
1327
- "at": "2026-05-17T05:48:07.187Z",
1327
+ "at": "2026-05-17T13:50:50.179Z",
1328
1328
  "operation": "create",
1329
1329
  "user": "platform@example.com",
1330
1330
  "groups": [
@@ -1375,7 +1375,7 @@
1375
1375
  }
1376
1376
  },
1377
1377
  "audit": {
1378
- "at": "2026-05-17T05:48:07.187Z",
1378
+ "at": "2026-05-17T13:50:50.179Z",
1379
1379
  "operation": "create",
1380
1380
  "user": "platform@example.com",
1381
1381
  "groups": [
@@ -1810,7 +1810,7 @@
1810
1810
  "maintainers"
1811
1811
  ],
1812
1812
  "invitedBy": "admin",
1813
- "expiresAt": "2026-05-24T05:48:07.187Z"
1813
+ "expiresAt": "2026-05-24T13:50:50.179Z"
1814
1814
  },
1815
1815
  "status": {
1816
1816
  "phase": "Pending",
@@ -2515,7 +2515,7 @@
2515
2515
  }
2516
2516
  },
2517
2517
  "audit": {
2518
- "at": "2026-05-17T05:48:07.185Z",
2518
+ "at": "2026-05-17T13:50:50.177Z",
2519
2519
  "operation": "create",
2520
2520
  "user": "admin@example.com",
2521
2521
  "groups": [
@@ -2556,7 +2556,7 @@
2556
2556
  }
2557
2557
  },
2558
2558
  "audit": {
2559
- "at": "2026-05-17T05:48:07.185Z",
2559
+ "at": "2026-05-17T13:50:50.178Z",
2560
2560
  "operation": "create",
2561
2561
  "user": "admin@example.com",
2562
2562
  "groups": [
@@ -2592,7 +2592,7 @@
2592
2592
  }
2593
2593
  },
2594
2594
  "audit": {
2595
- "at": "2026-05-17T05:48:07.186Z",
2595
+ "at": "2026-05-17T13:50:50.178Z",
2596
2596
  "operation": "create",
2597
2597
  "user": "admin@example.com",
2598
2598
  "groups": [
@@ -2637,7 +2637,7 @@
2637
2637
  }
2638
2638
  },
2639
2639
  "audit": {
2640
- "at": "2026-05-17T05:48:07.186Z",
2640
+ "at": "2026-05-17T13:50:50.178Z",
2641
2641
  "operation": "create",
2642
2642
  "user": "platform@example.com",
2643
2643
  "groups": [
@@ -2681,7 +2681,7 @@
2681
2681
  }
2682
2682
  },
2683
2683
  "audit": {
2684
- "at": "2026-05-17T05:48:07.186Z",
2684
+ "at": "2026-05-17T13:50:50.178Z",
2685
2685
  "operation": "create",
2686
2686
  "user": "admin@example.com",
2687
2687
  "groups": [
@@ -2729,7 +2729,7 @@
2729
2729
  }
2730
2730
  },
2731
2731
  "audit": {
2732
- "at": "2026-05-17T05:48:07.186Z",
2732
+ "at": "2026-05-17T13:50:50.178Z",
2733
2733
  "operation": "create",
2734
2734
  "user": "platform@example.com",
2735
2735
  "groups": [
@@ -2779,7 +2779,7 @@
2779
2779
  }
2780
2780
  },
2781
2781
  "audit": {
2782
- "at": "2026-05-17T05:48:07.186Z",
2782
+ "at": "2026-05-17T13:50:50.178Z",
2783
2783
  "operation": "create",
2784
2784
  "user": "platform@example.com",
2785
2785
  "groups": [
@@ -2830,7 +2830,7 @@
2830
2830
  }
2831
2831
  },
2832
2832
  "audit": {
2833
- "at": "2026-05-17T05:48:07.187Z",
2833
+ "at": "2026-05-17T13:50:50.178Z",
2834
2834
  "operation": "create",
2835
2835
  "user": "platform@example.com",
2836
2836
  "groups": [
@@ -2865,7 +2865,7 @@
2865
2865
  "maintainers"
2866
2866
  ],
2867
2867
  "invitedBy": "admin",
2868
- "expiresAt": "2026-05-24T05:48:07.187Z"
2868
+ "expiresAt": "2026-05-24T13:50:50.179Z"
2869
2869
  },
2870
2870
  "status": {
2871
2871
  "phase": "Pending",
@@ -2873,7 +2873,7 @@
2873
2873
  }
2874
2874
  },
2875
2875
  "audit": {
2876
- "at": "2026-05-17T05:48:07.187Z",
2876
+ "at": "2026-05-17T13:50:50.179Z",
2877
2877
  "operation": "create",
2878
2878
  "user": "admin@example.com",
2879
2879
  "groups": [
@@ -2924,7 +2924,7 @@
2924
2924
  }
2925
2925
  },
2926
2926
  "audit": {
2927
- "at": "2026-05-17T05:48:07.187Z",
2927
+ "at": "2026-05-17T13:50:50.179Z",
2928
2928
  "operation": "create",
2929
2929
  "user": "platform@example.com",
2930
2930
  "groups": [
@@ -2975,7 +2975,7 @@
2975
2975
  }
2976
2976
  },
2977
2977
  "audit": {
2978
- "at": "2026-05-17T05:48:07.187Z",
2978
+ "at": "2026-05-17T13:50:50.179Z",
2979
2979
  "operation": "create",
2980
2980
  "user": "platform@example.com",
2981
2981
  "groups": [
@@ -2990,7 +2990,7 @@
2990
2990
  ],
2991
2991
  "auditLog": [
2992
2992
  {
2993
- "at": "2026-05-17T05:48:07.185Z",
2993
+ "at": "2026-05-17T13:50:50.177Z",
2994
2994
  "operation": "create",
2995
2995
  "user": "admin@example.com",
2996
2996
  "groups": [
@@ -3002,7 +3002,7 @@
3002
3002
  "allowed": true
3003
3003
  },
3004
3004
  {
3005
- "at": "2026-05-17T05:48:07.185Z",
3005
+ "at": "2026-05-17T13:50:50.178Z",
3006
3006
  "operation": "create",
3007
3007
  "user": "admin@example.com",
3008
3008
  "groups": [
@@ -3014,7 +3014,7 @@
3014
3014
  "allowed": true
3015
3015
  },
3016
3016
  {
3017
- "at": "2026-05-17T05:48:07.186Z",
3017
+ "at": "2026-05-17T13:50:50.178Z",
3018
3018
  "operation": "create",
3019
3019
  "user": "admin@example.com",
3020
3020
  "groups": [
@@ -3026,7 +3026,7 @@
3026
3026
  "allowed": true
3027
3027
  },
3028
3028
  {
3029
- "at": "2026-05-17T05:48:07.186Z",
3029
+ "at": "2026-05-17T13:50:50.178Z",
3030
3030
  "operation": "create",
3031
3031
  "user": "platform@example.com",
3032
3032
  "groups": [
@@ -3038,7 +3038,7 @@
3038
3038
  "allowed": true
3039
3039
  },
3040
3040
  {
3041
- "at": "2026-05-17T05:48:07.186Z",
3041
+ "at": "2026-05-17T13:50:50.178Z",
3042
3042
  "operation": "create",
3043
3043
  "user": "admin@example.com",
3044
3044
  "groups": [
@@ -3050,7 +3050,7 @@
3050
3050
  "allowed": true
3051
3051
  },
3052
3052
  {
3053
- "at": "2026-05-17T05:48:07.186Z",
3053
+ "at": "2026-05-17T13:50:50.178Z",
3054
3054
  "operation": "create",
3055
3055
  "user": "platform@example.com",
3056
3056
  "groups": [
@@ -3062,7 +3062,7 @@
3062
3062
  "allowed": true
3063
3063
  },
3064
3064
  {
3065
- "at": "2026-05-17T05:48:07.186Z",
3065
+ "at": "2026-05-17T13:50:50.178Z",
3066
3066
  "operation": "create",
3067
3067
  "user": "platform@example.com",
3068
3068
  "groups": [
@@ -3074,7 +3074,7 @@
3074
3074
  "allowed": true
3075
3075
  },
3076
3076
  {
3077
- "at": "2026-05-17T05:48:07.187Z",
3077
+ "at": "2026-05-17T13:50:50.178Z",
3078
3078
  "operation": "create",
3079
3079
  "user": "platform@example.com",
3080
3080
  "groups": [
@@ -3086,7 +3086,7 @@
3086
3086
  "allowed": true
3087
3087
  },
3088
3088
  {
3089
- "at": "2026-05-17T05:48:07.187Z",
3089
+ "at": "2026-05-17T13:50:50.179Z",
3090
3090
  "operation": "create",
3091
3091
  "user": "admin@example.com",
3092
3092
  "groups": [
@@ -3098,7 +3098,7 @@
3098
3098
  "allowed": true
3099
3099
  },
3100
3100
  {
3101
- "at": "2026-05-17T05:48:07.187Z",
3101
+ "at": "2026-05-17T13:50:50.179Z",
3102
3102
  "operation": "create",
3103
3103
  "user": "platform@example.com",
3104
3104
  "groups": [
@@ -3110,7 +3110,7 @@
3110
3110
  "allowed": true
3111
3111
  },
3112
3112
  {
3113
- "at": "2026-05-17T05:48:07.187Z",
3113
+ "at": "2026-05-17T13:50:50.179Z",
3114
3114
  "operation": "create",
3115
3115
  "user": "platform@example.com",
3116
3116
  "groups": [
@@ -3,7 +3,7 @@
3
3
  "description": "Kubernetes-native forge runtime with Argo CD and Krate-managed repository hosting",
4
4
  "package": {
5
5
  "name": "@a5c-ai/krate",
6
- "version": "5.0.1-staging.c74b36109",
6
+ "version": "5.0.1-staging.ceeaa92b2",
7
7
  "private": true
8
8
  },
9
9
  "entrypoints": {
@@ -162,7 +162,7 @@
162
162
  "lifecycle": {
163
163
  "project": "Krate",
164
164
  "version": "0.1.0",
165
- "generatedAt": "2026-05-17T05:48:07.197Z",
165
+ "generatedAt": "2026-05-17T13:50:50.189Z",
166
166
  "status": "ready-for-local-development",
167
167
  "components": [
168
168
  {
@@ -464,7 +464,7 @@
464
464
  "ok": true,
465
465
  "assertions": []
466
466
  },
467
- "generatedAt": "2026-05-17T05:48:07.197Z",
467
+ "generatedAt": "2026-05-17T13:50:50.189Z",
468
468
  "controller": {
469
469
  "status": "degraded",
470
470
  "namespace": "krate-org-default",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a5c-ai/krate",
3
- "version": "5.0.1-staging.c74b36109",
3
+ "version": "5.0.1-staging.ceeaa92b2",
4
4
  "description": "a5c.ai Krate: Kubernetes-native forge runtime with Argo CD GitOps and Gitea-backed Git hosting.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -15,12 +15,14 @@ const required = [
15
15
  'apps/web/app/pages/settings-pages.jsx',
16
16
  'apps/web/app/pages/external-pages.jsx',
17
17
  'apps/web/proxy.js',
18
+ 'apps/web/app/components/app-settings.jsx',
18
19
  'apps/web/app/components/code-editor.jsx',
19
20
  'apps/web/app/components/resource-actions.jsx',
20
21
  'apps/web/app/components/issue-editor.jsx',
21
22
  'apps/web/app/components/repo-code-browser.jsx',
22
23
  'apps/web/app/components/repo-runs.jsx',
23
24
  'apps/web/app/components/krate-loading.jsx',
25
+ 'apps/web/app/components/theme-runtime.jsx',
24
26
  'apps/web/app/loading.jsx',
25
27
  'apps/web/app/api/controller/route.js',
26
28
  'apps/web/app/api/orgs/[org]/resources/route.js',
@@ -37,6 +39,7 @@ const required = [
37
39
  'apps/web/app/api/auth/callback/[provider]/route.js',
38
40
  'apps/web/app/api/auth/logout/route.js',
39
41
  'apps/web/app/api/auth/delegated/route.js',
42
+ 'apps/web/app/api/orgs/[org]/agents/events/stream/route.js',
40
43
  'src/api-controller.js',
41
44
  'src/kubernetes-resource-gateway.js',
42
45
  'src/kubernetes-controller.js',
@@ -136,9 +139,29 @@ for (const token of ['KrateControllerRecovery', 'KrateLoadingView', 'KRATE_LOADI
136
139
  for (const token of ['KrateRouteLoadingOverlay', 'krate-route-loading-refresh']) {
137
140
  if ((webUiSource() + files['apps/web/app/components/krate-loading.jsx']).includes(token)) failures.push(`route transitions must not render recovery loading UI token ${token}`);
138
141
  }
139
- if (!files['apps/web/app/loading.jsx'].includes('return null')) failures.push('route loading UI must stay silent during normal navigation');
142
+ if (!files['apps/web/app/loading.jsx'].includes('routeLoading') || !files['apps/web/app/loading.jsx'].includes('krateLoadingBar animated')) failures.push('route loading UI must render immediate non-overlay loading feedback');
143
+ if (files['apps/web/app/loading.jsx'].includes('KrateDelayedRouteLoading') || files['apps/web/app/loading.jsx'].includes('return null')) failures.push('route loading UI must not delay or render a blank fallback');
144
+ if (!files['apps/web/app/globals.css'].includes('krateRouteLoadingProgress') || !files['apps/web/app/globals.css'].includes('krateRouteLoadingPhase')) failures.push('route loading UI must animate progress and phase text without client hydration');
145
+ if (!files['apps/web/app/lib/krate-ui.jsx'].includes('useCache: true') || files['apps/web/app/lib/krate-ui.jsx'].includes('useCache: false')) failures.push('Krate page loader must use cached controller snapshots');
146
+ for (const token of ['KrateProject', 'Issue', 'syncHydratedModel', 'model.agents']) {
147
+ if (!files['apps/web/app/lib/krate-ui.jsx'].includes(token)) failures.push(`Krate page model hydration missing ${token}`);
148
+ if (!files['apps/web/app/api/controller/route.js'].includes(token === 'syncHydratedModel' ? 'KrateProject' : token)) failures.push(`controller API hydration missing ${token}`);
149
+ }
150
+ for (const token of ['ThemeRuntime', 'themeInitScript', 'krate-theme', 'suppressHydrationWarning']) {
151
+ if (!files['apps/web/app/layout.jsx'].includes(token)) failures.push(`root layout missing persistent theme token ${token}`);
152
+ }
153
+ for (const token of ['THEME_STORAGE_KEY', 'krate-theme', 'applyTheme', 'storeTheme', "window.addEventListener('storage'", 'prefers-color-scheme: dark']) {
154
+ if (!files['apps/web/app/components/theme-runtime.jsx'].includes(token)) failures.push(`theme runtime missing ${token}`);
155
+ }
156
+ for (const token of ['[style*="#374151"]', '[style*="#fafafa"]', 'outline: 3px solid #79c0ff', '::placeholder', 'background: #4d1512', '.pill.good { color: #7ee787', 'background: #ffb4ab', '[data-theme="dark"] h4', '.repoCommandBar > a']) {
157
+ if (!files['apps/web/app/globals.css'].includes(token)) failures.push(`dark mode accessibility override missing ${token}`);
158
+ }
159
+ if (!files['apps/web/app/components/app-settings.jsx'].includes('storeTheme(newTheme)') || files['apps/web/app/components/app-settings.jsx'].includes("localStorage.setItem('krate-theme'")) failures.push('settings theme changes must go through the shared theme runtime');
140
160
  if ((webUiSource() + files['apps/web/app/components/krate-loading.jsx']).includes('Krate workspace degraded or empty')) failures.push('degraded workspace copy should be replaced by recovery loading UI');
141
161
  if ((webUiSource() + files['apps/web/app/components/krate-loading.jsx']).includes('window.location.reload')) failures.push('recovery loading UI must not reload the page');
162
+ for (const token of ['text/event-stream', 'globalEventBus', 'KRATE_CONTROLLER_URL', "type: 'connected'"]) {
163
+ if (!files['apps/web/app/api/orgs/[org]/agents/events/stream/route.js'].includes(token)) failures.push(`events stream route missing ${token}`);
164
+ }
142
165
  if (!/\.krateRecoveryOverlay\s*\{[\s\S]*?position:\s*fixed;[\s\S]*?inset:\s*0;/.test(files['apps/web/app/globals.css'])) failures.push('recovery loading UI must be fixed overlay');
143
166
  for (const token of ['duplex', 'KRATE_GITEA_HTTP_URL', 'fetch(target', 'degraded']) {
144
167
  if (!files['apps/web/app/api/git-proxy/route.js'].includes(token)) failures.push(`git proxy route missing ${token}`);
@@ -19,6 +19,10 @@ export async function fetchControllerUiModel({ controllerUrl = process.env.KRATE
19
19
  if (localFallback && shouldFallbackFromRemoteModel(remoteModel)) {
20
20
  return fallbackControllerModel({ controller, connectionError: new Error(remoteControllerError(remoteModel) || 'controller returned degraded empty data'), organization, fallbackSnapshot });
21
21
  }
22
+ if (localFallback && shouldProbeLocalModel(remoteModel)) {
23
+ const localModel = await fallbackControllerModel({ controller, organization, fallbackSnapshot });
24
+ if (modelResourceScore(localModel) > modelResourceScore(remoteModel)) return localModel;
25
+ }
22
26
  return remoteModel;
23
27
  } catch (error) {
24
28
  if (localFallback) return fallbackControllerModel({ controller, connectionError: error, organization, fallbackSnapshot });
@@ -54,6 +58,29 @@ async function fallbackControllerModel({ controller = null, connectionError = nu
54
58
  }
55
59
  }
56
60
 
61
+
62
+ function shouldProbeLocalModel(model) {
63
+ if (!model || model.status !== 'ready') return false;
64
+ const hasLiveConnection = Boolean(model.controller?.connection?.available || model.controller?.apiService);
65
+ if (!hasLiveConnection) return false;
66
+ const summaries = Array.isArray(model.resources) ? model.resources : [];
67
+ const crdKinds = new Set(['Repository', 'RunnerPool', 'Pipeline', 'Job']);
68
+ const crdItems = summaries
69
+ .filter((resource) => crdKinds.has(resource?.kind))
70
+ .reduce((count, resource) => count + Number(resource?.count || resource?.items?.length || 0), 0);
71
+ return crdItems === 0;
72
+ }
73
+
74
+ function modelResourceScore(model) {
75
+ if (!model) return 0;
76
+ const metricCount = Number(model.metrics?.resources || 0);
77
+ const summaryCount = Array.isArray(model.resources)
78
+ ? model.resources.reduce((count, resource) => count + Number(resource?.count || resource?.items?.length || 0), 0)
79
+ : 0;
80
+ const dashboardCount = Number(model.views?.dashboard?.repositories?.length || 0);
81
+ return metricCount + summaryCount + dashboardCount;
82
+ }
83
+
57
84
  function shouldFallbackFromRemoteModel(model) {
58
85
  if (!model || model.status !== 'degraded') return false;
59
86
  const hasLiveConnection = Boolean(model.controller?.connection?.available || model.controller?.apiService);
@@ -264,13 +264,14 @@ function filterResourceItemsForOrg(definition, items = [], org) {
264
264
  return filterByOrg(items, org);
265
265
  }
266
266
 
267
- function filterByOrg(items = [], org) {
268
- if (!org) return items;
269
- return items.filter((item) => {
270
- const itemOrg = item.spec?.organizationRef || item.metadata?.labels?.[KRATE_ORG_LABEL];
271
- return itemOrg === org;
272
- });
273
- }
267
+ function filterByOrg(items = [], org) {
268
+ if (!org) return items;
269
+ const orgNamespace = orgNamespaceName(org);
270
+ return items.filter((item) => {
271
+ const itemOrg = item.spec?.organizationRef || item.metadata?.labels?.[KRATE_ORG_LABEL];
272
+ return itemOrg === org || item.metadata?.namespace === orgNamespace;
273
+ });
274
+ }
274
275
 
275
276
  function normalizeSnapshot(source = {}) {
276
277
  const raw = typeof source.snapshot === 'function' ? source.snapshot() : source;
@@ -212,7 +212,7 @@ export async function getControllerSnapshotAsync(options = {}) {
212
212
  // Fetch platform-scoped resources first so we can derive org namespaces
213
213
  const platformResults = await Promise.all(
214
214
  platformScopedDefs
215
- .filter((d) => discoveredPluralSet.has(`${d.group || KRATE_API_GROUP}/${d.plural}`))
215
+ .filter((d) => shouldListSnapshotDefinition(d, discoveredPluralSet))
216
216
  .map(async (definition) => {
217
217
  const resourceNamespace = definition.namespace || namespace;
218
218
  const result = await runKubectlAsync(
@@ -232,7 +232,7 @@ export async function getControllerSnapshotAsync(options = {}) {
232
232
  // Fetch org-scoped resources in parallel
233
233
  const orgResults = await Promise.all(
234
234
  orgScopedDefs
235
- .filter((d) => discoveredPluralSet.has(`${d.group || KRATE_API_GROUP}/${d.plural}`))
235
+ .filter((d) => shouldListSnapshotDefinition(d, discoveredPluralSet))
236
236
  .map(async (definition) => {
237
237
  const namespaces = definition.namespaced === false
238
238
  ? [null]
@@ -448,6 +448,12 @@ function namespaceArgs(definition, namespace) {
448
448
  return definition.namespaced === false ? [] : ['-n', namespace];
449
449
  }
450
450
 
451
+ function shouldListSnapshotDefinition(definition, discoveredPluralSet) {
452
+ const group = definition.group || KRATE_API_GROUP;
453
+ if (discoveredPluralSet.has(`${group}/${definition.plural}`)) return true;
454
+ return group === KRATE_API_GROUP;
455
+ }
456
+
451
457
  function parseKubernetesList(stdout) {
452
458
  const parsed = safeJson(stdout);
453
459
  if (!parsed) return { items: [] };
@@ -445,7 +445,7 @@ export async function getControllerSnapshot(options = {}) {
445
445
  const orgScopedDefinitions = snapshotResources.filter((definition) => !definition.platformScoped);
446
446
 
447
447
  for (const definition of platformScopedDefinitions) {
448
- if (!discoveredPluralSet.has(`${definition.group || KRATE_API_GROUP}/${definition.plural}`)) continue;
448
+ if (!shouldListSnapshotDefinition(definition, discoveredPluralSet)) continue;
449
449
  const resourceNamespace = definition.namespace || namespace;
450
450
  const result = runKubectl(['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true });
451
451
  listResults.push({ definition, result });
@@ -454,7 +454,7 @@ export async function getControllerSnapshot(options = {}) {
454
454
 
455
455
  const orgNamespaces = organizationNamespaces(resources.Organization, resources.OrgNamespaceBinding, namespace);
456
456
  for (const definition of orgScopedDefinitions) {
457
- if (!discoveredPluralSet.has(`${definition.group || KRATE_API_GROUP}/${definition.plural}`)) continue;
457
+ if (!shouldListSnapshotDefinition(definition, discoveredPluralSet)) continue;
458
458
  const namespaces = definition.namespaced === false ? [null] : [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
459
459
  resources[definition.kind] = namespaces.flatMap((resourceNamespace) => {
460
460
  const effectiveNamespace = resourceNamespace || namespace;
@@ -819,6 +819,12 @@ function organizationNamespaces(organizations = [], bindings = [], fallbackNames
819
819
  return fallbackOrgs.size ? [...fallbackOrgs] : [fallbackNamespace];
820
820
  }
821
821
 
822
+ function shouldListSnapshotDefinition(definition, discoveredPluralSet) {
823
+ const group = definition.group || KRATE_API_GROUP;
824
+ if (discoveredPluralSet.has(`${group}/${definition.plural}`)) return true;
825
+ return group === KRATE_API_GROUP;
826
+ }
827
+
822
828
  function parseKubernetesList(stdout) {
823
829
  const parsed = safeJson(stdout);
824
830
  if (!parsed) return { items: [] };
@@ -101,4 +101,33 @@ test('fetchControllerUiModel uses async fallback for degraded empty remote data
101
101
  assert.equal(model.status, 'ready');
102
102
  assert.equal(model.views.dashboard.repositories[0].metadata.name, 'test2');
103
103
  assert.ok(model.controller.connection.errors.length > 0);
104
- });
104
+ });
105
+
106
+
107
+ test('fetchControllerUiModel probes local snapshot when remote controller is ready but missing CRD-backed resources', async () => {
108
+ const calls = [];
109
+ const emptyRemote = createControllerUiModel({
110
+ source: 'kubernetes',
111
+ namespace: 'krate-staging',
112
+ kubectl: { available: true, context: 'aks-krate-staging', errors: [] },
113
+ apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } },
114
+ resources: {},
115
+ crds: [],
116
+ events: [],
117
+ permissions: [],
118
+ storage: {},
119
+ commands: []
120
+ }, { organization: 'default' });
121
+
122
+ const model = await fetchControllerUiModel({
123
+ controllerUrl: 'http://krate-api.krate-staging.svc.cluster.local',
124
+ organization: 'default',
125
+ useCache: false,
126
+ fetchImpl: async () => ({ ok: true, json: async () => emptyRemote }),
127
+ fallbackSnapshot: async () => { calls.push('fallbackSnapshot'); return liveSnapshot(); }
128
+ });
129
+
130
+ assert.deepEqual(calls, ['fallbackSnapshot']);
131
+ assert.equal(model.metrics.repositories, 1);
132
+ assert.equal(model.views.dashboard.repositories[0].metadata.name, 'test2');
133
+ });
@@ -236,7 +236,7 @@ test('web UI is wired to the Kubernetes controller API instead of a static local
236
236
  assert.ok(gateway.includes('repositoryManifest'));
237
237
  assert.ok(shell.includes('/api/controller'));
238
238
  assert.ok(webControllerRoute.includes('KRATE_CONTROLLER_URL'));
239
- assert.ok(!webControllerRoute.includes('createKrateApiController'), 'web API route proxies the controller service instead of shelling out through local kubectl');
239
+ assert.ok(webControllerRoute.includes('hydrateOrgResourceSummaries'), 'web API route hydrates empty controller summaries from org-scoped resources');
240
240
  assert.ok(shell.includes('ArchitectureMap'));
241
241
  assert.ok(shell.includes('Repository home'));
242
242
  assert.ok(shell.includes('IssueWorkspace'));
@@ -329,6 +329,29 @@ test('Delegated identity route redirects localhost fallback even when Kubernetes
329
329
  }
330
330
  });
331
331
 
332
+
333
+
334
+ test('controller UI model keeps namespace-scoped resources visible for their org', () => {
335
+ const model = createControllerUiModel({
336
+ source: 'kubernetes',
337
+ namespace: 'krate-system',
338
+ generatedAt: 'test-time',
339
+ kubectl: { available: true, context: 'kind-krate', errors: [] },
340
+ crds: [],
341
+ resources: {
342
+ Organization: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'krate-system' }, spec: { slug: 'default', namespaceName: 'krate-org-default' } }],
343
+ RunnerPool: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'RunnerPool', metadata: { name: 'default', namespace: 'krate-org-default' }, spec: { image: 'ubuntu:24.04' } }]
344
+ },
345
+ events: [],
346
+ permissions: [],
347
+ storage: {},
348
+ commands: []
349
+ }, { organization: 'default' });
350
+
351
+ assert.equal(model.metrics.runnerPools, 1);
352
+ assert.deepEqual(model.resources.find((resource) => resource.kind === 'RunnerPool').names, ['default']);
353
+ });
354
+
332
355
  test('Krate delivery resources surface through controller UI model', () => {
333
356
  const model = createControllerUiModel({
334
357
  source: 'kubernetes',
@@ -116,6 +116,41 @@ test('getControllerSnapshotAsync uses in-cluster service account instead of kube
116
116
  await rm(tempDir, { recursive: true, force: true });
117
117
  }
118
118
  });
119
+
120
+
121
+ test('full async snapshot lists known Krate CRDs even when CRD discovery is empty', async () => {
122
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'krate-known-crds-'));
123
+ const fakePath = path.join(tempDir, 'fake-kubectl.cjs');
124
+ const fakeScript = `
125
+ const args = process.argv.slice(1);
126
+ const joined = args.join(' ');
127
+ const has = (value) => args.includes(value) || joined.includes(value);
128
+ if (has('current-context')) { console.log('kind-krate'); process.exit(0); }
129
+ if (has('version')) { console.log(JSON.stringify({ clientVersion: { gitVersion: 'v1.test' } })); process.exit(0); }
130
+ if (has('apiservice')) { console.log(JSON.stringify({ metadata: { name: 'v1alpha1.krate.a5c.ai' } })); process.exit(0); }
131
+ if (has('crd')) { console.error('crd discovery unavailable'); process.exit(1); }
132
+ if (has('runnerpools.krate.a5c.ai')) {
133
+ console.log(JSON.stringify({ items: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'RunnerPool', metadata: { name: 'default', namespace: 'krate-org-default', labels: { 'krate.a5c.ai/org': 'default' } }, spec: { organizationRef: 'default', image: 'ubuntu:24.04' } }] }));
134
+ process.exit(0);
135
+ }
136
+ console.log(JSON.stringify({ items: [] }));
137
+ process.exit(0);
138
+ `;
139
+
140
+ try {
141
+ await writeFile(fakePath, fakeScript);
142
+ const snapshot = await getControllerSnapshotAsync({
143
+ kubectl: process.execPath,
144
+ timeoutMs: 1000,
145
+ env: { KRATE_ORG: 'default', NODE_OPTIONS: `--require ${fakePath}` }
146
+ });
147
+ assert.equal(snapshot.resources.RunnerPool.length, 1);
148
+ assert.equal(snapshot.resources.RunnerPool[0].metadata.name, 'default');
149
+ } finally {
150
+ await rm(tempDir, { recursive: true, force: true });
151
+ }
152
+ });
153
+
119
154
  // ---------------------------------------------------------------------------
120
155
  // getPartialSnapshot
121
156
  // ---------------------------------------------------------------------------